Merge remote-tracking branch 'origin/develop' into harden-parser

This commit is contained in:
Henry Jameson 2023-06-05 21:53:14 +03:00
commit 5e656cc0b4
124 changed files with 5370 additions and 1409 deletions

View file

@ -580,8 +580,6 @@ textarea,
}
&[type="checkbox"] {
display: none;
&:checked + label::before {
color: $fallback--text;
color: var(--inputText, $fallback--text);
@ -647,6 +645,20 @@ option {
}
}
.cards-list {
list-style: none;
display: grid;
grid-auto-flow: row dense;
grid-template-columns: 1fr 1fr;
li {
border: 1px solid var(--border);
border-radius: var(--inputRadius);
padding: 0.5em;
margin: 0.25em;
}
}
.btn-block {
display: block;
width: 100%;
@ -657,16 +669,19 @@ option {
display: inline-flex;
vertical-align: middle;
button {
button,
.button-dropdown {
position: relative;
flex: 1 1 auto;
&:not(:last-child) {
&:not(:last-child),
&:not(:last-child) .button-default {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:not(:first-child) {
&:not(:first-child),
&:not(:first-child) .button-default {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
@ -887,3 +902,15 @@ option {
opacity: 0;
}
/* stylelint-enable no-descending-specificity */
.visible-for-screenreader-only {
display: block;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
visibility: visible;
clip: rect(0 0 0 0);
padding: 0;
position: absolute;
}

View file

@ -253,6 +253,7 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') })
store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') })
store.dispatch('setInstanceOption', { name: 'pleromaCustomEmojiReactionsAvailable', value: features.includes('pleroma_custom_emoji_reactions') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'editingAvailable', value: features.includes('editing') })

View file

@ -1,16 +1,21 @@
<template>
<label
class="checkbox"
:class="{ disabled, indeterminate }"
:class="{ disabled, indeterminate, 'indeterminate-fix': indeterminateTransitionFix }"
>
<input
type="checkbox"
class="visible-for-screenreader-only"
:disabled="disabled"
:checked="modelValue"
:indeterminate="indeterminate"
@change="$emit('update:modelValue', $event.target.checked)"
>
<i class="checkbox-indicator" />
<i
class="checkbox-indicator"
:aria-hidden="true"
@transitionend.capture="onTransitionEnd"
/>
<span
v-if="!!$slots.default"
class="label"
@ -27,12 +32,30 @@ export default {
'indeterminate',
'disabled'
],
emits: ['update:modelValue']
emits: ['update:modelValue'],
data: (vm) => ({
indeterminateTransitionFix: vm.indeterminate
}),
watch: {
indeterminate (e) {
if (e) {
this.indeterminateTransitionFix = true
}
}
},
methods: {
onTransitionEnd (e) {
if (!this.indeterminate) {
this.indeterminateTransitionFix = false
}
}
}
}
</script>
<style lang="scss">
@import "../../variables";
@import "../../mixins";
.checkbox {
position: relative;
@ -81,8 +104,6 @@ export default {
}
input[type="checkbox"] {
display: none;
&:checked + .checkbox-indicator::before {
color: $fallback--text;
color: var(--inputText, $fallback--text);
@ -95,6 +116,12 @@ export default {
}
}
&.indeterminate-fix {
input[type="checkbox"] + .checkbox-indicator::before {
content: "";
}
}
& > span {
margin-left: 0.5em;
}

View file

@ -107,7 +107,10 @@ export default {
this.searchBarHidden = hidden
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
this.$store.dispatch('openSettingsModal', 'user')
},
openAdminModal () {
this.$store.dispatch('openSettingsModal', 'admin')
}
}
}

View file

@ -20,6 +20,7 @@
class="logo"
:to="{ name: 'root' }"
:style="logoBgStyle"
:title="sitename"
>
<div
class="mask"
@ -38,40 +39,39 @@
/>
<button
class="button-unstyled nav-icon"
:title="$t('nav.preferences')"
@click.stop="openSettingsModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="cog"
:title="$t('nav.preferences')"
/>
</button>
<a
<button
v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma"
class="nav-icon"
class="button-unstyled nav-icon"
target="_blank"
@click.stop
:title="$t('nav.administration')"
@click.stop="openAdminModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="tachometer-alt"
:title="$t('nav.administration')"
/>
</a>
</button>
<span class="spacer" />
<button
v-if="currentUser"
class="button-unstyled nav-icon"
:title="$t('login.logout')"
@click.stop.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
:title="$t('login.logout')"
/>
</button>
</div>

View file

@ -1,6 +1,7 @@
import Completion from '../../services/completion/completion.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import Popover from 'src/components/popover/popover.vue'
import ScreenReaderNotice from 'src/components/screen_reader_notice/screen_reader_notice.vue'
import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue'
import { take } from 'lodash'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
@ -109,9 +110,10 @@ const EmojiInput = {
},
data () {
return {
randomSeed: `${Math.random()}`.replace('.', '-'),
input: undefined,
caretEl: undefined,
highlighted: 0,
highlighted: -1,
caret: 0,
focused: false,
blurTimeout: null,
@ -125,12 +127,16 @@ const EmojiInput = {
components: {
Popover,
EmojiPicker,
UnicodeDomainIndicator
UnicodeDomainIndicator,
ScreenReaderNotice
},
computed: {
padEmoji () {
return this.$store.getters.mergedConfig.padEmoji
},
defaultCandidateIndex () {
return this.$store.getters.mergedConfig.autocompleteSelect ? 0 : -1
},
preText () {
return this.modelValue.slice(0, this.caret)
},
@ -203,6 +209,12 @@ const EmojiInput = {
top: this.input.scrollTop,
left: this.input.scrollLeft
})
},
suggestionListId () {
return `suggestions-${this.randomSeed}`
},
suggestionItemId () {
return (index) => `suggestion-item-${index}-${this.randomSeed}`
}
},
mounted () {
@ -278,6 +290,11 @@ const EmojiInput = {
...rest,
img: imageUrl || ''
}))
this.highlighted = this.defaultCandidateIndex
this.$refs.screenReaderNotice.announce(
this.$tc('tool_tip.autocomplete_available',
this.suggestions.length,
{ number: this.suggestions.length }))
}
},
methods: {
@ -374,26 +391,27 @@ const EmojiInput = {
},
cycleBackward (e) {
const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted -= 1
if (this.highlighted < 0) {
this.highlighted = this.suggestions.length - 1
}
this.highlighted -= 1
if (this.highlighted === -1) {
this.input.focus()
} else if (this.highlighted < -1) {
this.highlighted = len - 1
}
if (len > 0) {
e.preventDefault()
} else {
this.highlighted = 0
}
},
cycleForward (e) {
const len = this.suggestions.length || 0
if (len > 1) {
this.highlighted += 1
if (this.highlighted >= len) {
this.highlighted = 0
}
this.highlighted += 1
if (this.highlighted >= len) {
this.highlighted = -1
this.input.focus()
}
if (len > 0) {
e.preventDefault()
} else {
this.highlighted = 0
}
},
scrollIntoView () {
@ -540,6 +558,13 @@ const EmojiInput = {
})
},
resize () {
},
autoCompleteItemLabel (suggestion) {
if (suggestion.user) {
return suggestion.displayText + ' ' + suggestion.detailText
} else {
return this.maybeLocalizedEmojiName(suggestion)
}
}
}
}

View file

@ -4,12 +4,19 @@
class="emoji-input"
:class="{ 'with-picker': !hideEmojiButton }"
>
<slot />
<slot
:id="'textbox-' + randomSeed"
:aria-owns="suggestionListId"
aria-autocomplete="both"
:aria-expanded="showSuggestions"
:aria-activedescendant="(!showSuggestions || highlighted === -1) ? '' : suggestionItemId(highlighted)"
/>
<!-- TODO: make the 'x' disappear if at the end maybe? -->
<div
ref="hiddenOverlay"
class="hidden-overlay"
:style="overlayStyle"
:aria-hidden="true"
>
<span>{{ preText }}</span>
<span
@ -18,11 +25,16 @@
>x</span>
<span>{{ postText }}</span>
</div>
<screen-reader-notice
ref="screenReaderNotice"
aria-live="assertive"
/>
<template v-if="enableEmojiPicker">
<button
v-if="!hideEmojiButton"
class="button-unstyled emoji-picker-icon"
type="button"
:title="$t('emoji.add_emoji')"
@click.prevent="togglePicker"
>
<FAIcon :icon="['far', 'smile-beam']" />
@ -43,17 +55,24 @@
ref="suggestorPopover"
class="autocomplete-panel"
placement="bottom"
:trigger-attrs="{ 'aria-hidden': true }"
>
<template #content>
<div
:id="suggestionListId"
ref="panel-body"
class="autocomplete-panel-body"
role="listbox"
>
<div
v-for="(suggestion, index) in suggestions"
:id="suggestionItemId(index)"
:key="index"
class="autocomplete-item"
role="option"
:class="{ highlighted: index === highlighted }"
:aria-label="autoCompleteItemLabel(suggestion)"
:aria-selected="index === highlighted"
@click.stop.prevent="onClick($event, suggestion)"
>
<span class="image">

View file

@ -94,8 +94,9 @@ export const suggestUsers = ({ dispatch, state }) => {
const newSuggestions = state.users.users.filter(
user =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)
user.screen_name && user.name && (
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix))
).slice(0, 20).sort((a, b) => {
let aScore = 0
let bScore = 0

View file

@ -98,6 +98,11 @@ const EmojiPicker = {
required: false,
type: Boolean,
default: false
},
hideCustomEmoji: {
required: false,
type: Boolean,
default: false
}
},
data () {
@ -280,6 +285,9 @@ const EmojiPicker = {
return 0
},
allCustomGroups () {
if (this.hideCustomEmoji) {
return {}
}
const emojis = this.$store.getters.groupedCustomEmojis
if (emojis.unpacked) {
emojis.unpacked.text = this.$t('emoji.unpacked')

View file

@ -3,6 +3,7 @@
ref="popover"
trigger="click"
popover-class="emoji-picker popover-default"
:trigger-attrs="{ 'aria-hidden': true }"
@show="onPopoverShown"
@close="onPopoverClosed"
>

View file

@ -1,5 +1,17 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserListPopover from '../user_list_popover/user_list_popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faPlus,
faMinus,
faCheck
} from '@fortawesome/free-solid-svg-icons'
library.add(
faPlus,
faMinus,
faCheck
)
const EMOJI_REACTION_COUNT_CUTOFF = 12
@ -33,6 +45,9 @@ const EmojiReactions = {
},
loggedIn () {
return !!this.$store.state.users.currentUser
},
remoteInteractionLink () {
return this.$store.getters.remoteInteractionLink({ statusId: this.status.id })
}
},
methods: {
@ -62,6 +77,17 @@ const EmojiReactions = {
} else {
this.reactWith(emoji)
}
},
counterTriggerAttrs (reaction) {
return {
class: [
'btn',
'button-default',
'emoji-reaction-count-button',
{ '-picked-reaction': this.reactedWith(reaction.name) }
],
'aria-label': this.$tc('status.reaction_count_label', reaction.count, { num: reaction.count })
}
}
}
}

View file

@ -1,20 +1,64 @@
<template>
<div class="EmojiReactions">
<UserListPopover
<span
v-for="(reaction) in emojiReactions"
:key="reaction.name"
:users="accountsForEmoji[reaction.name]"
:key="reaction.url || reaction.name"
class="emoji-reaction-container btn-group"
>
<button
<component
:is="loggedIn ? 'button' : 'a'"
v-bind="!loggedIn ? { href: remoteInteractionLink } : {}"
role="button"
class="emoji-reaction btn button-default"
:class="{ '-picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
:class="{ '-picked-reaction': reactedWith(reaction.name) }"
:title="reaction.url ? reaction.name : undefined"
:aria-pressed="reactedWith(reaction.name)"
@click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"
>
<span class="reaction-emoji">{{ reaction.name }}</span>
<span>{{ reaction.count }}</span>
</button>
</UserListPopover>
<span
class="reaction-emoji"
>
<img
v-if="reaction.url"
:src="reaction.url"
class="reaction-emoji-content"
width="1em"
>
<span
v-else
class="reaction-emoji reaction-emoji-content"
>{{ reaction.name }}</span>
</span>
<FALayers>
<FAIcon
v-if="reactedWith(reaction.name)"
class="active-marker"
transform="shrink-6 up-9"
icon="check"
/>
<FAIcon
v-if="!reactedWith(reaction.name)"
class="focus-marker"
transform="shrink-6 up-9"
icon="plus"
/>
<FAIcon
v-else
class="focus-marker"
transform="shrink-6 up-9"
icon="minus"
/>
</FALayers>
</component>
<UserListPopover
:users="accountsForEmoji[reaction.name]"
class="emoji-reaction-popover"
:trigger-attrs="counterTriggerAttrs(reaction)"
@show="fetchEmojiReactionsByIfMissing()"
>
<span class="emoji-reaction-counts">{{ reaction.count }}</span>
</UserListPopover>
</span>
<a
v-if="tooManyReactions"
class="emoji-reaction-expand faint"
@ -29,43 +73,118 @@
<script src="./emoji_reactions.js"></script>
<style lang="scss">
@import "../../variables";
@import "../../mixins";
.EmojiReactions {
display: flex;
margin-top: 0.25em;
flex-wrap: wrap;
.emoji-reaction {
padding: 0 0.5em;
margin-right: 0.5em;
--emoji-size: calc(1.25em * var(--emojiReactionsScale, 1));
.emoji-reaction-container {
display: flex;
align-items: stretch;
margin-top: 0.5em;
margin-right: 0.5em;
.emoji-reaction-popover {
padding: 0;
.emoji-reaction-count-button {
background-color: var(--btn);
height: 100%;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
box-sizing: border-box;
min-width: 2em;
display: inline-flex;
justify-content: center;
align-items: center;
color: $fallback--text;
color: var(--btnText, $fallback--text);
&.-picked-reaction {
border: 1px solid var(--accent, $fallback--link);
margin-right: -1px;
}
}
}
}
.emoji-reaction {
padding-left: 0.5em;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
.reaction-emoji {
width: 1.25em;
width: var(--emoji-size);
height: var(--emoji-size);
margin-right: 0.25em;
line-height: var(--emoji-size);
display: flex;
justify-content: center;
align-items: center;
}
.reaction-emoji-content {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
line-height: inherit;
overflow: hidden;
font-size: calc(var(--emoji-size) * 0.8);
margin: 0;
}
&:focus {
outline: none;
}
&.not-clickable {
cursor: default;
&:hover {
box-shadow: $fallback--buttonShadow;
box-shadow: var(--buttonShadow);
}
.svg-inline--fa {
color: $fallback--text;
color: var(--btnText, $fallback--text);
}
&.-picked-reaction {
border: 1px solid var(--accent, $fallback--link);
margin-left: -1px; // offset the border, can't use inset shadows either
margin-right: calc(0.5em - 1px);
margin-right: -1px;
.svg-inline--fa {
color: $fallback--link;
color: var(--accent, $fallback--link);
}
}
@include unfocused-style {
.focus-marker {
visibility: hidden;
}
.active-marker {
visibility: visible;
}
}
@include focused-style {
.svg-inline--fa {
color: $fallback--link;
color: var(--accent, $fallback--link);
}
.focus-marker {
visibility: visible;
}
.active-marker {
visibility: hidden;
}
}
}

View file

@ -38,13 +38,20 @@
class="button-unstyled interactive"
target="_blank"
role="button"
:title="$t('tool_tip.favorite')"
:href="remoteInteractionLink"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.favorite')"
:icon="['far', 'star']"
/>
<FALayers class="fa-scale-110 fa-old-padding-layer">
<FAIcon
class="fa-scale-110"
:icon="['far', 'star']"
/>
<FAIcon
class="focus-marker"
transform="shrink-6 up-9 right-12"
icon="plus"
/>
</FALayers>
</a>
<span
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"

View file

@ -4,6 +4,7 @@
:class="{ custom: isCustom }"
>
<label
:id="name + '-label'"
:for="preset === 'custom' ? name : name + '-font-switcher'"
class="label"
>
@ -12,7 +13,8 @@
<input
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
class="opt exlcude-disabled"
:aria-labelledby="name + '-label'"
class="opt exlcude-disabled visible-for-screenreader-only"
type="checkbox"
:checked="present"
@change="$emit('update:modelValue', typeof modelValue === 'undefined' ? fallback : undefined)"
@ -21,6 +23,7 @@
v-if="typeof fallback !== 'undefined'"
class="opt-l"
:for="name + '-o'"
:aria-hidden="true"
/>
{{ ' ' }}
<Select

View file

@ -36,7 +36,9 @@
<button
class="button-default btn"
@click="addLanguage"
>{{ $t('settings.add_language') }}</button>
>
{{ $t('settings.add_language') }}
</button>
</li>
</ul>
</div>
@ -102,7 +104,7 @@ export default {
</script>
<style lang="scss">
@import '../../_variables.scss';
@import "../../variables";
.interface-language-switcher {
.language-select {

View file

@ -1,9 +1,13 @@
<template>
<div class="list">
<div
class="list"
role="list"
>
<div
v-for="item in items"
:key="getKey(item)"
class="list-item"
role="listitem"
>
<slot
name="item"

View file

@ -23,6 +23,11 @@ const mediaUpload = {
}
},
methods: {
onClick () {
if (this.uploadReady) {
this.$refs.input.click()
}
},
uploadFile (file) {
const self = this
const store = this.$store
@ -69,10 +74,15 @@ const mediaUpload = {
this.multiUpload(target.files)
}
},
props: [
'dropFiles',
'disabled'
],
props: {
dropFiles: Object,
disabled: Boolean,
normalButton: Boolean,
acceptTypes: {
type: String,
default: '*/*'
}
},
watch: {
dropFiles: function (fileInfos) {
if (!this.uploading) {

View file

@ -1,8 +1,9 @@
<template>
<label
<button
class="media-upload"
:class="{ disabled: disabled }"
:class="[normalButton ? 'button-default btn' : 'button-unstyled', { disabled }]"
:title="$t('tool_tip.media_upload')"
@click="onClick"
>
<FAIcon
v-if="uploading"
@ -15,15 +16,21 @@
class="new-icon"
icon="upload"
/>
<template v-if="normalButton">
{{ ' ' }}
{{ uploading ? $t('general.loading') : $t('tool_tip.media_upload') }}
</template>
<input
v-if="uploadReady"
ref="input"
class="hidden-input-file"
:disabled="disabled"
type="file"
multiple="true"
:accept="acceptTypes"
@change="change"
>
</label>
</button>
</template>
<script src="./media_upload.js"></script>
@ -32,10 +39,12 @@
@import "../../variables";
.media-upload {
cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
.hidden-input-file {
display: none;
}
}
</style>
label.media-upload {
cursor: pointer; // We use <label> for interactivity... i wonder if it's fine
}
</style>

View file

@ -80,3 +80,21 @@ export const ROOT_ITEMS = {
criteria: ['announcements']
}
}
export function routeTo (item, currentUser) {
if (!item.route && !item.routeObject) return null
let route
if (item.routeObject) {
route = item.routeObject
} else {
route = { name: (item.anon || currentUser) ? item.route : item.anonRoute }
}
if (USERNAME_ROUTES.has(route.name)) {
route.params = { username: currentUser.screen_name, name: currentUser.screen_name }
}
return route
}

View file

@ -1,5 +1,5 @@
import { mapState } from 'vuex'
import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
import { routeTo } from 'src/components/navigation/navigation.js'
import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faThumbtack } from '@fortawesome/free-solid-svg-icons'
@ -26,17 +26,7 @@ const NavigationEntry = {
},
computed: {
routeTo () {
if (!this.item.route && !this.item.routeObject) return null
let route
if (this.item.routeObject) {
route = this.item.routeObject
} else {
route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute }
}
if (USERNAME_ROUTES.has(route.name)) {
route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name }
}
return route
return routeTo(this.item, this.currentUser)
},
getters () {
return this.$store.getters

View file

@ -1,5 +1,5 @@
import { mapState } from 'vuex'
import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js'
import { TIMELINES, ROOT_ITEMS, routeTo } from 'src/components/navigation/navigation.js'
import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js'
import { library } from '@fortawesome/fontawesome-svg-core'
@ -31,14 +31,7 @@ const NavPanel = {
props: ['limit'],
methods: {
getRouteTo (item) {
if (item.routeObject) {
return item.routeObject
}
const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute }
if (USERNAME_ROUTES.has(route.name)) {
route.params = { username: this.currentUser.screen_name }
}
return route
return routeTo(item, this.currentUser)
}
},
computed: {
@ -52,6 +45,7 @@ const NavPanel = {
privateMode: state => state.instance.private,
federating: state => state.instance.federating,
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
supportsAnnouncements: state => state.announcements.supportsAnnouncements,
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems)
}),
pinnedList () {
@ -63,6 +57,7 @@ const NavPanel = {
],
{
hasChats: this.pleromaChatMessagesAvailable,
hasAnnouncements: this.supportsAnnouncements,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser
@ -82,6 +77,7 @@ const NavPanel = {
],
{
hasChats: this.pleromaChatMessagesAvailable,
hasAnnouncements: this.supportsAnnouncements,
isFederating: this.federating,
isPrivate: this.privateMode,
currentUser: this.currentUser

View file

@ -121,7 +121,17 @@
scope="global"
keypath="notifications.reacted_with"
>
<span class="emoji-reaction-emoji">{{ notification.emoji }}</span>
<img
v-if="notification.emoji_url"
class="emoji-reaction-emoji emoji-reaction-emoji-image"
:src="notification.emoji_url"
:alt="notification.emoji"
:title="notification.emoji"
>
<span
v-else
class="emoji-reaction-emoji"
>{{ notification.emoji }}</span>
</i18n-t>
</small>
</span>
@ -153,9 +163,9 @@
</router-link>
<button
class="button-unstyled expand-icon"
@click.prevent="toggleStatusExpanded"
:title="$t('tool_tip.toggle_expand')"
:aria-expanded="statusExpanded"
@click.prevent="toggleStatusExpanded"
>
<FAIcon
class="fa-scale-110"

View file

@ -129,6 +129,13 @@
.emoji-reaction-emoji {
font-size: 1.3em;
max-width: 1.25em;
height: 1.25em;
width: auto;
}
.emoji-reaction-emoji-image {
vertical-align: middle;
}
.notification-details {

View file

@ -12,7 +12,8 @@ export default {
data () {
return {
loading: false,
choices: []
choices: [],
randomSeed: `${Math.random()}`.replace('.', '-')
}
},
created () {

View file

@ -4,53 +4,63 @@
:class="containerClass"
>
<div
v-for="(option, index) in options"
:key="index"
class="poll-option"
:role="showResults ? 'section' : (poll.multiple ? 'group' : 'radiogroup')"
>
<div
v-if="showResults"
:title="resultTitle(option)"
class="option-result"
v-for="(option, index) in options"
:key="index"
class="poll-option"
>
<div class="option-result-label">
<span class="result-percentage">
{{ percentageForOption(option.votes_count) }}%
</span>
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
<div
v-if="showResults"
:title="resultTitle(option)"
class="option-result"
>
<div class="option-result-label">
<span class="result-percentage">
{{ percentageForOption(option.votes_count) }}%
</span>
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</div>
<div
class="result-fill"
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
/>
</div>
<div
class="result-fill"
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
/>
</div>
<div
v-else
@click="activateOption(index)"
>
<input
v-if="poll.multiple"
type="checkbox"
:disabled="loading"
:value="index"
>
<input
v-else
type="radio"
:disabled="loading"
:value="index"
tabindex="0"
:role="poll.multiple ? 'checkbox' : 'radio'"
:aria-labelledby="`option-vote-${randomSeed}-${index}`"
:aria-checked="choices[index]"
@click="activateOption(index)"
>
<label class="option-vote">
<RichContent
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</label>
<input
v-if="poll.multiple"
type="checkbox"
class="poll-checkbox"
:disabled="loading"
:value="index"
>
<input
v-else
type="radio"
:disabled="loading"
:value="index"
>
<label class="option-vote">
<RichContent
:id="`option-vote-${randomSeed}-${index}`"
:html="option.title_html"
:handle-links="false"
:emoji="emoji"
/>
</label>
</div>
</div>
</div>
<div class="footer faint">
@ -161,5 +171,9 @@
padding: 0 0.5em;
margin-right: 0.5em;
}
.poll-checkbox {
display: none;
}
}
</style>

View file

@ -45,6 +45,9 @@ const Popover = {
// Lets hover popover stay when clicking inside of it
stayOnClick: Boolean,
// Use styled button (to avoid nested buttons)
normalButton: Boolean,
triggerAttrs: {
type: Object,
default: {}

View file

@ -5,7 +5,8 @@
>
<button
ref="trigger"
class="button-unstyled popover-trigger-button"
class="popover-trigger-button"
:class="normalButton ? 'button-default btn' : 'button-unstyled'"
type="button"
v-bind="triggerAttrs"
@click="onClick"

View file

@ -8,6 +8,7 @@ import Gallery from 'src/components/gallery/gallery.vue'
import StatusContent from '../status_content/status_content.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { propsToNative } from '../../services/attributes_helper/attributes_helper.service.js'
import { reject, map, uniqBy, debounce } from 'lodash'
import suggestor from '../emoji_input/suggestor.js'
import { mapGetters, mapState } from 'vuex'
@ -629,6 +630,9 @@ const PostStatusForm = {
},
openProfileTab () {
this.$store.dispatch('openSettingsModalTab', 'profile')
},
propsToNative (props) {
return propsToNative(props)
}
}
}

View file

@ -30,6 +30,9 @@
<span>{{ $t('post_status.scope_notice.public') }}</span>
<a
class="fa-scale-110 fa-old-padding dismiss"
:title="$t('post_status.scope_notice_dismiss')"
role="button"
tabindex="0"
@click.prevent="dismissScopeNotice()"
>
<FAIcon icon="times" />
@ -42,6 +45,9 @@
<span>{{ $t('post_status.scope_notice.unlisted') }}</span>
<a
class="fa-scale-110 fa-old-padding dismiss"
:title="$t('post_status.scope_notice_dismiss')"
role="button"
tabindex="0"
@click.prevent="dismissScopeNotice()"
>
<FAIcon icon="times" />
@ -54,6 +60,9 @@
<span>{{ $t('post_status.scope_notice.private') }}</span>
<a
class="fa-scale-110 fa-old-padding dismiss"
:title="$t('post_status.scope_notice_dismiss')"
role="button"
tabindex="0"
@click.prevent="dismissScopeNotice()"
>
<FAIcon icon="times" />
@ -124,14 +133,17 @@
:suggest="emojiSuggestor"
class="form-control"
>
<input
v-model="newStatus.spoilerText"
type="text"
:placeholder="$t('post_status.content_warning')"
:disabled="posting && !optimisticPosting"
size="1"
class="form-post-subject"
>
<template #default="inputProps">
<input
v-model="newStatus.spoilerText"
type="text"
:placeholder="$t('post_status.content_warning')"
:disabled="posting && !optimisticPosting"
v-bind="propsToNative(inputProps)"
size="1"
class="form-post-subject"
>
</template>
</EmojiInput>
<EmojiInput
ref="emoji-input"
@ -148,29 +160,32 @@
@sticker-upload-failed="uploadFailed"
@shown="handleEmojiInputShow"
>
<textarea
ref="textarea"
v-model="newStatus.status"
:placeholder="placeholder || $t('post_status.default')"
rows="1"
cols="1"
:disabled="posting && !optimisticPosting"
class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight }"
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
@keydown.meta.enter="postStatus($event, newStatus)"
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
@input="resize"
@compositionupdate="resize"
@paste="paste"
/>
<p
v-if="hasStatusLengthLimit"
class="character-counter faint"
:class="{ error: isOverLengthLimit }"
>
{{ charactersLeft }}
</p>
<template #default="inputProps">
<textarea
ref="textarea"
v-model="newStatus.status"
:placeholder="placeholder || $t('post_status.default')"
rows="1"
cols="1"
:disabled="posting && !optimisticPosting"
class="form-post-body"
:class="{ 'scrollable-form': !!maxHeight }"
v-bind="propsToNative(inputProps)"
@keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)"
@keydown.meta.enter="postStatus($event, newStatus)"
@keydown.ctrl.enter="!submitOnEnter && postStatus($event, newStatus)"
@input="resize"
@compositionupdate="resize"
@paste="paste"
/>
<p
v-if="hasStatusLengthLimit"
class="character-counter faint"
:class="{ error: isOverLengthLimit }"
>
{{ charactersLeft }}
</p>
</template>
</EmojiInput>
<div
v-if="!disableScopeSelector"
@ -193,6 +208,7 @@
id="post-content-type"
v-model="newStatus.contentType"
class="form-control"
:attrs="{ 'aria-label': $t('post_status.content_type_selection') }"
>
<option
v-for="postFormat in postFormats"
@ -265,12 +281,10 @@
>
{{ $t('post_status.post') }}
</button>
<!-- touchstart is used to keep the OSK at the same position after a message send -->
<button
v-else
:disabled="uploadingFiles || disableSubmit"
class="btn button-default"
@touchstart.stop.prevent="postStatus($event, newStatus)"
@click.stop.prevent="postStatus($event, newStatus)"
>
{{ $t('post_status.post') }}

View file

@ -6,36 +6,51 @@
:trigger-attrs="{ title: $t('timeline.quick_filter_settings') }"
>
<template #content>
<div class="dropdown-menu">
<div v-if="loggedIn">
<div
class="dropdown-menu"
role="menu"
>
<div
v-if="loggedIn"
role="group"
>
<button
v-if="!conversation"
class="button-default dropdown-item"
:aria-checked="replyVisibilityAll"
role="menuitemradio"
@click="replyVisibilityAll = true"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilityAll }"
:aria-hidden="true"
/>{{ $t('settings.reply_visibility_all') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
:aria-checked="replyVisibilityFollowing"
role="menuitemradio"
@click="replyVisibilityFollowing = true"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilityFollowing }"
:aria-hidden="true"
/>{{ $t('settings.reply_visibility_following_short') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
:aria-checked="replyVisibilitySelf"
role="menuitemradio"
@click="replyVisibilitySelf = true"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': replyVisibilitySelf }"
:aria-hidden="true"
/>{{ $t('settings.reply_visibility_self_short') }}
</button>
<div
@ -46,33 +61,43 @@
</div>
<button
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="muteBotStatuses"
@click="muteBotStatuses = !muteBotStatuses"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': muteBotStatuses }"
:aria-hidden="true"
/>{{ $t('settings.mute_bot_posts') }}
</button>
<button
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="hideMedia"
@click="hideMedia = !hideMedia"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hideMedia }"
:aria-hidden="true"
/>{{ $t('settings.hide_media_previews') }}
</button>
<button
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="hideMutedPosts"
@click="hideMutedPosts = !hideMutedPosts"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': hideMutedPosts }"
:aria-hidden="true"
/>{{ $t('settings.hide_all_muted_posts') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click="openTab('filtering')"
>
<FAIcon icon="font" />{{ $t('settings.word_filter_and_more') }}

View file

@ -6,60 +6,87 @@
:trigger-attrs="{ title: $t('timeline.quick_view_settings') }"
>
<template #content>
<div class="dropdown-menu">
<button
class="button-default dropdown-item"
@click="conversationDisplay = 'tree'"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
/><FAIcon icon="folder-tree" /> {{ $t('settings.conversation_display_tree_quick') }}
</button>
<button
class="button-default dropdown-item"
@click="conversationDisplay = 'linear'"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
/><FAIcon icon="list" /> {{ $t('settings.conversation_display_linear_quick') }}
</button>
<div
class="dropdown-menu"
role="menu"
>
<div role="group">
<button
class="button-default dropdown-item"
:aria-checked="conversationDisplay === 'tree'"
role="menuitemradio"
@click="conversationDisplay = 'tree'"
>
<span
class="menu-checkbox -radio"
:aria-hidden="true"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'tree' }"
/><FAIcon
icon="folder-tree"
:aria-hidden="true"
/> {{ $t('settings.conversation_display_tree_quick') }}
</button>
<button
class="button-default dropdown-item"
:aria-checked="conversationDisplay === 'linear'"
role="menuitemradio"
@click="conversationDisplay = 'linear'"
>
<span
class="menu-checkbox -radio"
:class="{ 'menu-checkbox-checked': conversationDisplay === 'linear' }"
:aria-hidden="true"
/><FAIcon
icon="list"
:aria-hidden="true"
/> {{ $t('settings.conversation_display_linear_quick') }}
</button>
</div>
<div
role="separator"
class="dropdown-divider"
/>
<button
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="showUserAvatars"
@click="showUserAvatars = !showUserAvatars"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': showUserAvatars }"
:aria-hidden="true"
/>{{ $t('settings.mention_link_show_avatar_quick') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="autoUpdate"
@click="autoUpdate = !autoUpdate"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': autoUpdate }"
:aria-hidden="true"
/>{{ $t('settings.auto_update') }}
</button>
<button
v-if="!conversation"
class="button-default dropdown-item"
role="menuitemcheckbox"
:aria-checked="collapseWithSubjects"
@click="collapseWithSubjects = !collapseWithSubjects"
>
<span
class="menu-checkbox"
:class="{ 'menu-checkbox-checked': collapseWithSubjects }"
:aria-hidden="true"
/>{{ $t('settings.collapse_subject') }}
</button>
<button
class="button-default dropdown-item dropdown-item-icon"
role="menuitem"
@click="openTab('general')"
>
<FAIcon icon="wrench" />{{ $t('settings.more_settings') }}

View file

@ -4,6 +4,7 @@
:class="{ disabled: !present || disabled }"
>
<label
:id="name + '-label'"
:for="name"
class="label"
>
@ -12,7 +13,8 @@
<input
v-if="typeof fallback !== 'undefined'"
:id="name + '-o'"
class="opt"
:aria-labelledby="name + '-label'"
class="opt visible-for-screenreader-only"
type="checkbox"
:checked="present"
@change="$emit('update:modelValue', !present ? fallback : undefined)"
@ -21,6 +23,7 @@
v-if="typeof fallback !== 'undefined'"
class="opt-l"
:for="name + '-o'"
:aria-hidden="true"
/>
<input
:id="name"
@ -34,9 +37,10 @@
@input="$emit('update:modelValue', $event.target.value)"
>
<input
:id="name"
:id="name + '-numeric'"
class="input-number"
type="number"
:aria-labelledby="name + '-label'"
:value="modelValue || fallback"
:disabled="!present || disabled"
:max="hardMax"

View file

@ -1,9 +1,8 @@
import Popover from '../popover/popover.vue'
import { ensureFinalFallback } from '../../i18n/languages.js'
import EmojiPicker from '../emoji_picker/emoji_picker.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faPlus, faTimes } from '@fortawesome/free-solid-svg-icons'
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
import { trim } from 'lodash'
library.add(
faPlus,
@ -20,105 +19,34 @@ const ReactButton = {
}
},
components: {
Popover
Popover,
EmojiPicker
},
methods: {
addReaction (event, emoji, close) {
addReaction (event) {
const emoji = event.insertion
const existingReaction = this.status.emoji_reactions.find(r => r.name === emoji)
if (existingReaction && existingReaction.me) {
this.$store.dispatch('unreactWithEmoji', { id: this.status.id, emoji })
} else {
this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji })
}
close()
},
show () {
if (!this.expanded) {
this.$refs.picker.showPicker()
}
},
onShow () {
this.expanded = true
this.focusInput()
},
onClose () {
this.expanded = false
},
focusInput () {
this.$nextTick(() => {
const input = document.querySelector('.reaction-picker-filter > input')
if (input) input.focus()
})
},
// Vaguely adjusted copypaste from emoji_input and emoji_picker!
maybeLocalizedEmojiNamesAndKeywords (emoji) {
const names = [emoji.displayText]
const keywords = []
if (emoji.displayTextI18n) {
names.push(this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args))
}
if (emoji.annotations) {
this.languages.forEach(lang => {
names.push(emoji.annotations[lang]?.name)
keywords.push(...(emoji.annotations[lang]?.keywords || []))
})
}
return {
names: names.filter(k => k),
keywords: keywords.filter(k => k)
}
},
maybeLocalizedEmojiName (emoji) {
if (!emoji.annotations) {
return emoji.displayText
}
if (emoji.displayTextI18n) {
return this.$t(emoji.displayTextI18n.key, emoji.displayTextI18n.args)
}
for (const lang of this.languages) {
if (emoji.annotations[lang]?.name) {
return emoji.annotations[lang].name
}
}
return emoji.displayText
}
},
computed: {
commonEmojis () {
const hardcodedSet = new Set(['👍', '😠', '👀', '😂', '🔥'])
return this.$store.getters.standardEmojiList.filter(emoji => hardcodedSet.has(emoji.replacement))
},
languages () {
return ensureFinalFallback(this.$store.getters.mergedConfig.interfaceLanguage)
},
emojis () {
if (this.filterWord !== '') {
const keywordLowercase = trim(this.filterWord.toLowerCase())
const orderedEmojiList = []
for (const emoji of this.$store.getters.standardEmojiList) {
const indices = this.maybeLocalizedEmojiNamesAndKeywords(emoji)
.keywords
.map(k => k.toLowerCase().indexOf(keywordLowercase))
.filter(k => k > -1)
const indexOfKeyword = indices.length ? Math.min(...indices) : -1
if (indexOfKeyword > -1) {
if (!Array.isArray(orderedEmojiList[indexOfKeyword])) {
orderedEmojiList[indexOfKeyword] = []
}
orderedEmojiList[indexOfKeyword].push(emoji)
}
}
return orderedEmojiList.flat()
}
return this.$store.getters.standardEmojiList || []
},
mergedConfig () {
return this.$store.getters.mergedConfig
hideCustomEmoji () {
return !this.$store.state.instance.pleromaCustomEmojiReactionsAvailable
}
}
}

View file

@ -1,73 +1,39 @@
<template>
<Popover
trigger="click"
class="ReactButton"
placement="top"
:offset="{ y: 5 }"
:bound-to="{ x: 'container' }"
remove-padding
popover-class="ReactButton popover-default"
@show="onShow"
@close="onClose"
>
<template #content="{close}">
<div class="reaction-picker-filter">
<input
v-model="filterWord"
size="1"
:placeholder="$t('emoji.search_emoji')"
@input="$event.target.composing = false"
>
</div>
<div class="reaction-picker">
<span
v-for="emoji in commonEmojis"
:key="emoji.replacement"
class="emoji-button"
:title="maybeLocalizedEmojiName(emoji)"
@click="addReaction($event, emoji.replacement, close)"
>
{{ emoji.replacement }}
</span>
<div class="reaction-picker-divider" />
<span
v-for="(emoji, key) in emojis"
:key="key"
class="emoji-button"
:title="maybeLocalizedEmojiName(emoji)"
@click="addReaction($event, emoji.replacement, close)"
>
{{ emoji.replacement }}
</span>
<div class="reaction-bottom-fader" />
</div>
</template>
<template #trigger>
<span
class="button-unstyled popover-trigger"
:title="$t('tool_tip.add_reaction')"
>
<FALayers>
<FAIcon
class="fa-scale-110 fa-old-padding"
:icon="['far', 'smile-beam']"
/>
<FAIcon
v-show="!expanded"
class="focus-marker"
transform="shrink-6 up-9 right-17"
icon="plus"
/>
<FAIcon
v-show="expanded"
class="focus-marker"
transform="shrink-6 up-9 right-17"
icon="times"
/>
</FALayers>
</span>
</template>
</Popover>
<span class="ReactButton">
<EmojiPicker
ref="picker"
:enable-sticker-picker="enableStickerPicker"
:hide-custom-emoji="hideCustomEmoji"
class="emoji-picker-panel"
@emoji="addReaction"
@show="onShow"
@close="onClose"
/>
<span
class="button-unstyled popover-trigger"
:title="$t('tool_tip.add_reaction')"
@click.stop.prevent="show"
>
<FALayers>
<FAIcon
class="fa-scale-110 fa-old-padding"
:icon="['far', 'smile-beam']"
/>
<FAIcon
v-show="!expanded"
class="focus-marker"
transform="shrink-6 up-9 right-17"
icon="plus"
/>
<FAIcon
v-show="expanded"
class="focus-marker"
transform="shrink-6 up-9 right-17"
icon="times"
/>
</FALayers>
</span>
</span>
</template>
<script src="./react_button.js"></script>
@ -135,11 +101,6 @@
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
.popover-trigger-button {
/* override of popover internal stuff */
width: auto;
@include unfocused-style {
.focus-marker {

View file

@ -16,7 +16,7 @@ const registration = {
confirm: '',
birthday: '',
reason: '',
language: ''
language: ['']
},
captcha: {}
}),
@ -100,7 +100,7 @@ const registration = {
this.user.captcha_token = this.captcha.token
this.user.captcha_answer_data = this.captcha.answer_data
if (this.user.language) {
this.user.language = localeService.internalToBackendLocale(this.user.language)
this.user.language = localeService.internalToBackendLocaleMulti(this.user.language.filter(k => k))
}
this.v$.$touch()

View file

@ -210,6 +210,7 @@
:prompt-text="$t('registration.email_language')"
:language="v$.user.language.$model"
:set-language="val => v$.user.language.$model = val"
@click.stop.prevent
/>
</div>

View file

@ -32,12 +32,20 @@
target="_blank"
role="button"
:href="remoteInteractionLink"
:title="$t('tool_tip.reply')"
>
<FAIcon
icon="reply"
class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.reply')"
/>
<FALayers class="fa-old-padding-layer">
<FAIcon
class="fa-scale-110"
icon="reply"
/>
<FAIcon
v-if="!replying"
class="focus-marker"
transform="shrink-6 up-8 right-16"
icon="plus"
/>
</FALayers>
</a>
<span
v-if="status.replies_count > 0"

View file

@ -45,13 +45,20 @@
class="button-unstyled interactive"
target="_blank"
role="button"
:title="$t('tool_tip.repeat')"
:href="remoteInteractionLink"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="retweet"
:title="$t('tool_tip.repeat')"
/>
<FALayers class="fa-old-padding-layer">
<FAIcon
class="fa-scale-110"
icon="retweet"
/>
<FAIcon
class="focus-marker"
transform="shrink-6 up-9 right-12"
icon="plus"
/>
</FALayers>
</a>
<span
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"

View file

@ -0,0 +1,21 @@
const ScreenReaderNotice = {
props: {
ariaLive: {
type: String,
defualt: 'assertive'
}
},
data () {
return {
currentText: ''
}
},
methods: {
announce (text) {
this.currentText = text
setTimeout(() => { this.currentText = '' }, 1000)
}
}
}
export default ScreenReaderNotice

View file

@ -0,0 +1,10 @@
<template>
<div
class="visible-for-screenreader-only"
:aria-live="ariaLive"
>
{{ currentText }}
</div>
</template>
<script src="./screen_reader_notice.js"></script>

View file

@ -8,6 +8,7 @@
class="button-unstyled nav-icon"
:title="$t('nav.search')"
type="button"
:aria-expanded="!hidden"
@click.prevent.stop="toggleHidden"
>
<FAIcon
@ -29,6 +30,7 @@
<button
class="button-default search-button"
type="submit"
:title="$t('nav.search')"
@click="find(searchTerm)"
>
<FAIcon
@ -39,6 +41,8 @@
<button
class="button-unstyled cancel-search"
type="button"
:title="$t('nav.search_close')"
:aria-expanded="!hidden"
@click.prevent.stop="toggleHidden"
>
<FAIcon

View file

@ -13,6 +13,7 @@ export default {
'modelValue',
'disabled',
'unstyled',
'kind'
'kind',
'attrs'
]
}

View file

@ -6,6 +6,7 @@
<select
:disabled="disabled"
:value="modelValue"
v-bind="attrs"
@change="$emit('update:modelValue', $event.target.value)"
>
<slot />

View file

@ -0,0 +1,64 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import StringSetting from '../helpers/string_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import Popover from 'src/components/popover/popover.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const FrontendsTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
GroupSetting,
Popover
},
created () {
if (this.user.rights.admin) {
this.$store.dispatch('loadFrontendsStuff')
}
},
computed: {
frontends () {
return this.$store.state.adminSettings.frontends
},
...SharedComputedObject()
},
methods: {
update (frontend, suggestRef) {
const ref = suggestRef || frontend.refs[0]
const { name } = frontend
const payload = { name, ref }
this.$store.state.api.backendInteractor.installFrontend({ payload })
.then((externalUser) => {
this.$store.dispatch('loadFrontendsStuff')
})
},
setDefault (frontend, suggestRef) {
const ref = suggestRef || frontend.refs[0]
const { name } = frontend
this.$store.commit('updateAdminDraft', { path: [':pleroma', ':frontends', ':primary'], value: { name, ref } })
}
}
}
export default FrontendsTab

View file

@ -0,0 +1,13 @@
.frontends-tab {
.cards-list {
padding: 0;
}
dd {
text-overflow: ellipsis;
word-wrap: nowrap;
white-space: nowrap;
overflow-x: hidden;
max-width: 10em;
}
}

View file

@ -0,0 +1,184 @@
<template>
<div
class="frontends-tab"
:label="$t('admin_dash.tabs.frontends')"
>
<div class="setting-item">
<h2>{{ $t('admin_dash.tabs.frontends') }}</h2>
<p>{{ $t('admin_dash.frontend.wip_notice') }}</p>
<ul class="setting-list">
<li>
<h3>{{ $t('admin_dash.frontend.default_frontend') }}</h3>
<p>{{ $t('admin_dash.frontend.default_frontend_tip') }}</p>
<p>{{ $t('admin_dash.frontend.default_frontend_tip2') }}</p>
<ul class="setting-list">
<li>
<StringSetting path=":pleroma.:frontends.:primary.name" />
</li>
<li>
<StringSetting path=":pleroma.:frontends.:primary.ref" />
</li>
<li>
<GroupSetting path=":pleroma.:frontends.:primary" />
</li>
</ul>
</li>
</ul>
<div class="setting-list">
<h3>{{ $t('admin_dash.frontend.available_frontends') }}</h3>
<ul class="cards-list">
<li
v-for="frontend in frontends"
:key="frontend.name"
>
<strong>{{ frontend.name }}</strong>
{{ ' ' }}
<span v-if="adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name">
<i18n-t
v-if="adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]"
keypath="admin_dash.frontend.is_default"
/>
<i18n-t
v-else
keypath="admin_dash.frontend.is_default_custom"
>
<template #version>
<code>{{ adminDraft[':pleroma'][':frontends'][':primary'].ref }}</code>
</template>
</i18n-t>
</span>
<dl>
<dt>{{ $t('admin_dash.frontend.repository') }}</dt>
<dd>
<a
:href="frontend.git"
target="_blank"
>{{ frontend.git }}</a>
</dd>
<template v-if="expertLevel">
<dt>{{ $t('admin_dash.frontend.versions') }}</dt>
<dd
v-for="ref in frontend.refs"
:key="ref"
>
<code>{{ ref }}</code>
</dd>
</template>
<dt v-if="expertLevel">
{{ $t('admin_dash.frontend.build_url') }}
</dt>
<dd v-if="expertLevel">
<a
:href="frontend.build_url"
target="_blank"
>{{ frontend.build_url }}</a>
</dd>
</dl>
<div>
<span class="btn-group">
<button
class="button button-default btn"
type="button"
@click="update(frontend)"
>
{{
frontend.installed
? $t('admin_dash.frontend.reinstall')
: $t('admin_dash.frontend.install')
}}
</button>
<Popover
v-if="frontend.refs.length > 1"
trigger="click"
class="button-dropdown"
placement="bottom"
>
<template #content>
<div class="dropdown-menu">
<button
v-for="ref in frontend.refs"
:key="ref"
class="button-default dropdown-item"
@click="update(frontend, ref)"
>
<i18n-t keypath="admin_dash.frontend.install_version">
<template #version>
<code>{{ ref }}</code>
</template>
</i18n-t>
</button>
</div>
</template>
<template #trigger>
<button
class="button button-default btn dropdown-button"
type="button"
:title="$t('admin_dash.frontend.more_install_options')"
>
<FAIcon icon="chevron-down" />
</button>
</template>
</Popover>
</span>
<span
v-if="frontend.installed && frontend.name !== 'admin-fe'"
class="btn-group"
>
<button
class="button button-default btn"
type="button"
:disabled="
adminDraft[':pleroma'][':frontends'][':primary'].name === frontend.name &&
adminDraft[':pleroma'][':frontends'][':primary'].ref === frontend.refs[0]
"
@click="setDefault(frontend)"
>
{{
$t('admin_dash.frontend.set_default')
}}
</button>
{{ ' ' }}
<Popover
v-if="frontend.refs.length > 1"
trigger="click"
class="button-dropdown"
placement="bottom"
>
<template #content>
<div class="dropdown-menu">
<button
v-for="ref in frontend.refs.slice(1)"
:key="ref"
class="button-default dropdown-item"
@click="setDefault(frontend, ref)"
>
<i18n-t keypath="admin_dash.frontend.set_default_version">
<template #version>
<code>{{ ref }}</code>
</template>
</i18n-t>
</button>
</div>
</template>
<template #trigger>
<button
class="button button-default btn dropdown-button"
type="button"
:title="$t('admin_dash.frontend.more_default_options')"
>
<FAIcon icon="chevron-down" />
</button>
</template>
</Popover>
</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script src="./frontends_tab.js"></script>
<style lang="scss" src="./frontends_tab.scss"></style>

View file

@ -0,0 +1,38 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import StringSetting from '../helpers/string_setting.vue'
import GroupSetting from '../helpers/group_setting.vue'
import AttachmentSetting from '../helpers/attachment_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const InstanceTab = {
provide () {
return {
defaultDraftMode: true,
defaultSource: 'admin'
}
},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting,
AttachmentSetting,
GroupSetting
},
computed: {
...SharedComputedObject()
}
}
export default InstanceTab

View file

@ -0,0 +1,196 @@
<template>
<div :label="$t('admin_dash.tabs.instance')">
<div class="setting-item">
<h2>{{ $t('admin_dash.instance.instance') }}</h2>
<ul class="setting-list">
<li>
<StringSetting path=":pleroma.:instance.:name" />
</li>
<li>
<StringSetting path=":pleroma.:instance.:email" />
</li>
<li>
<StringSetting path=":pleroma.:instance.:description" />
</li>
<li>
<StringSetting path=":pleroma.:instance.:short_description" />
</li>
<li>
<AttachmentSetting path=":pleroma.:instance.:instance_thumbnail" />
</li>
<li>
<AttachmentSetting path=":pleroma.:instance.:background_image" />
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('admin_dash.instance.registrations') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting path=":pleroma.:instance.:registrations_open" />
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path=":pleroma.:instance.:invites_enabled"
parent-path=":pleroma.:instance.:registrations_open"
parent-invert
/>
</li>
</ul>
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:birthday_required" />
<ul class="setting-list suboptions">
<li>
<IntegerSetting
path=":pleroma.:instance.:birthday_min_age"
parent-path=":pleroma.:instance.:birthday_required"
/>
</li>
</ul>
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:account_activation_required" />
</li>
<li>
<BooleanSetting path=":pleroma.:instance.:account_approval_required" />
</li>
<li>
<h3>{{ $t('admin_dash.instance.captcha_header') }}</h3>
<ul class="setting-list">
<li>
<BooleanSetting :path="[':pleroma', 'Pleroma.Captcha', ':enabled']" />
<ul class="setting-list suboptions">
<li>
<ChoiceSetting
:path="[':pleroma', 'Pleroma.Captcha', ':method']"
:parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
:option-label-map="{
'Pleroma.Captcha.Native': $t('admin_dash.captcha.native'),
'Pleroma.Captcha.Kocaptcha': $t('admin_dash.captcha.kocaptcha')
}"
/>
<IntegerSetting
:path="[':pleroma', 'Pleroma.Captcha', ':seconds_valid']"
:parent-path="[':pleroma', 'Pleroma.Captcha', ':enabled']"
/>
</li>
<li
v-if="adminDraft[':pleroma']['Pleroma.Captcha'][':enabled'] && adminDraft[':pleroma']['Pleroma.Captcha'][':method'] === 'Pleroma.Captcha.Kocaptcha'"
>
<h4>{{ $t('admin_dash.instance.kocaptcha') }}</h4>
<ul class="setting-list">
<li>
<StringSetting :path="[':pleroma', 'Pleroma.Captcha.Kocaptcha', ':endpoint']" />
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('admin_dash.instance.access') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting
override-backend-description
override-backend-description-label
path=":pleroma.:instance.:public"
/>
</li>
<li>
<ChoiceSetting
override-backend-description
override-backend-description-label
path=":pleroma.:instance.:limit_to_local_content"
/>
</li>
<li v-if="expertLevel">
<h3>{{ $t('admin_dash.instance.restrict.header') }}</h3>
<p>
{{ $t('admin_dash.instance.restrict.description') }}
</p>
<ul class="setting-list">
<li>
<h4>{{ $t('admin_dash.instance.restrict.timelines') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:timelines.:local"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:timelines.:federated"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<GroupSetting path=":pleroma.:restrict_unauthenticated.:timelines" />
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.instance.restrict.profiles') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:profiles.:local"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:profiles.:remote"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<GroupSetting path=":pleroma.:restrict_unauthenticated.:profiles" />
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.instance.restrict.activities') }}</h4>
<ul class="setting-list">
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:activities.:local"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<BooleanSetting
path=":pleroma.:restrict_unauthenticated.:activities.:remote"
indeterminate-state=":if_instance_is_private"
swap-description-and-label
hide-description
/>
</li>
<li>
<GroupSetting path=":pleroma.:restrict_unauthenticated.:activities" />
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script src="./instance_tab.js"></script>

View file

@ -0,0 +1,29 @@
import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import StringSetting from '../helpers/string_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
} from '@fortawesome/free-solid-svg-icons'
library.add(
faGlobe
)
const LimitsTab = {
data () {},
components: {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
StringSetting
},
computed: {
...SharedComputedObject()
}
}
export default LimitsTab

View file

@ -0,0 +1,136 @@
<template>
<div :label="$t('admin_dash.tabs.limits')">
<div class="setting-item">
<h2>{{ $t('admin_dash.limits.arbitrary_limits') }}</h2>
<ul class="setting-list">
<li>
<h3>{{ $t('admin_dash.limits.posts') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:limit"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:remote_limit"
expert="1"
draft-mode
/>
</li>
</ul>
</li>
<li>
<h3>{{ $t('admin_dash.limits.uploads') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:description_limit"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:upload_limit"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_media_attachments"
draft-mode
/>
</li>
</ul>
</li>
<li>
<h3>{{ $t('admin_dash.limits.users') }}</h3>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_pinned_statuses"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:user_bio_length"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:user_name_length"
draft-mode
/>
</li>
<li>
<h4>{{ $t('admin_dash.limits.profile_fields') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_account_fields"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:max_remote_account_fields"
draft-mode
expert="1"
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:account_field_name_length"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:account_field_value_length"
draft-mode
/>
</li>
</ul>
</li>
<li>
<h4>{{ $t('admin_dash.limits.user_uploads') }}</h4>
<ul class="setting-list">
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:avatar_upload_limit"
draft-mode
/>
</li>
<li>
<IntegerSetting
source="admin"
path=":pleroma.:instance.:banner_upload_limit"
draft-mode
/>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script src="./limits_tab.js"></script>

View file

@ -0,0 +1,43 @@
import Setting from './setting.js'
import { fileTypeExt } from 'src/services/file_type/file_type.service.js'
import MediaUpload from 'src/components/media_upload/media_upload.vue'
import Attachment from 'src/components/attachment/attachment.vue'
export default {
...Setting,
props: {
...Setting.props,
acceptTypes: {
type: String,
required: false,
default: 'image/*'
}
},
components: {
...Setting.components,
MediaUpload,
Attachment
},
computed: {
...Setting.computed,
attachment () {
const path = this.realDraftMode ? this.draft : this.state
// The "server" part is primarily for local dev, but could be useful for alt-domain or multiuser usage.
const url = path.includes('://') ? path : this.$store.state.instance.server + path
return {
mimetype: fileTypeExt(url),
url
}
}
},
methods: {
...Setting.methods,
setMediaFile (fileInfo) {
if (this.realDraftMode) {
this.draft = fileInfo.url
} else {
this.configSink(this.path, fileInfo.url)
}
}
}
}

View file

@ -0,0 +1,96 @@
<template>
<span
v-if="matchesExpertLevel"
class="AttachmentSetting"
>
<label
:for="path"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</label>
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
<div class="attachment-input">
<div>{{ $t('settings.url') }}</div>
<div class="controls">
<input
:id="path"
class="string-input"
:disabled="shouldBeDisabled"
:value="realDraftMode ? draft : state"
@change="update"
>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
</div>
<div>{{ $t('settings.preview') }}</div>
<Attachment
class="attachment"
:compact="compact"
:attachment="attachment"
size="small"
hide-description
@setMedia="onMedia"
@naturalSizeLoad="onNaturalSizeLoad"
/>
<div class="controls">
<MediaUpload
ref="mediaUpload"
class="media-upload-icon"
:drop-files="dropFiles"
normal-button
:accept-types="acceptTypes"
@uploaded="setMediaFile"
@upload-failed="uploadFailed"
/>
</div>
</div>
<DraftButtons />
</span>
</template>
<script src="./attachment_setting.js"></script>
<style lang="scss">
.AttachmentSetting {
.attachment {
display: block;
width: 100%;
height: 15em;
margin-bottom: 0.5em;
}
.attachment-input {
margin-left: 1em;
display: flex;
flex-direction: column;
width: 20em;
}
.controls {
margin-bottom: 0.5em;
input,
button {
width: 100%;
}
}
}
</style>

View file

@ -1,56 +1,31 @@
import { get, set } from 'lodash'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import ModifiedIndicator from './modified_indicator.vue'
import ServerSideIndicator from './server_side_indicator.vue'
import Setting from './setting.js'
export default {
components: {
Checkbox,
ModifiedIndicator,
ServerSideIndicator
...Setting,
props: {
...Setting.props,
indeterminateState: [String, Object]
},
components: {
...Setting.components,
Checkbox
},
props: [
'path',
'disabled',
'expert'
],
computed: {
pathDefault () {
const [firstSegment, ...rest] = this.path.split('.')
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
state () {
const value = get(this.$parent, this.path)
if (value === undefined) {
return this.defaultState
} else {
return value
}
},
defaultState () {
return get(this.$parent, this.pathDefault)
},
isServerSide () {
return this.path.startsWith('serverSide_')
},
isChanged () {
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$parent.expertLevel
...Setting.computed,
isIndeterminate () {
return this.visibleState === this.indeterminateState
}
},
methods: {
update (e) {
const [firstSegment, ...rest] = this.path.split('.')
set(this.$parent, this.path, e)
// Updating nested properties does not trigger update on its parent.
// probably still not as reliable, but works for depth=1 at least
if (rest.length > 0) {
set(this.$parent, firstSegment, { ...get(this.$parent, firstSegment) })
...Setting.methods,
getValue (e) {
// Basic tri-state toggle implementation
if (!!this.indeterminateState && !e && this.visibleState === true) {
// If we have indeterminate state, switching from true to false first goes through indeterminate
return this.indeterminateState
}
},
reset () {
set(this.$parent, this.path, this.defaultState)
return e
}
}
}

View file

@ -4,23 +4,37 @@
class="BooleanSetting"
>
<Checkbox
:model-value="state"
:disabled="disabled"
:model-value="visibleState"
:disabled="shouldBeDisabled"
:indeterminate="isIndeterminate"
@update:modelValue="update"
>
<span
v-if="!!$slots.default"
class="label"
:class="{ 'faint': shouldBeDisabled }"
>
<slot />
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</span>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ServerSideIndicator :server-side="isServerSide" />
</Checkbox>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</label>
</template>

View file

@ -1,51 +1,41 @@
import { get, set } from 'lodash'
import Select from 'src/components/select/select.vue'
import ModifiedIndicator from './modified_indicator.vue'
import ServerSideIndicator from './server_side_indicator.vue'
import Setting from './setting.js'
export default {
...Setting,
components: {
Select,
ModifiedIndicator,
ServerSideIndicator
...Setting.components,
Select
},
props: {
...Setting.props,
options: {
type: Array,
required: false
},
optionLabelMap: {
type: Object,
required: false,
default: {}
}
},
props: [
'path',
'disabled',
'options',
'expert'
],
computed: {
pathDefault () {
const [firstSegment, ...rest] = this.path.split('.')
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
state () {
const value = get(this.$parent, this.path)
if (value === undefined) {
return this.defaultState
} else {
return value
...Setting.computed,
realOptions () {
if (this.realSource === 'admin') {
return this.backendDescriptionSuggestions.map(x => ({
key: x,
value: x,
label: this.optionLabelMap[x] || x
}))
}
},
defaultState () {
return get(this.$parent, this.pathDefault)
},
isServerSide () {
return this.path.startsWith('serverSide_')
},
isChanged () {
return !this.path.startsWith('serverSide_') && this.state !== this.defaultState
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$parent.expertLevel
return this.options
}
},
methods: {
update (e) {
set(this.$parent, this.path, e)
},
reset () {
set(this.$parent, this.path, this.defaultState)
...Setting.methods,
getValue (e) {
return e
}
}
}

View file

@ -3,15 +3,20 @@
v-if="matchesExpertLevel"
class="ChoiceSetting"
>
<slot />
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel }}
</template>
<template v-else>
<slot />
</template>
{{ ' ' }}
<Select
:model-value="state"
:model-value="realDraftMode ? draft :state"
:disabled="disabled"
@update:modelValue="update"
>
<option
v-for="option in options"
v-for="option in realOptions"
:key="option.key"
:value="option.value"
>
@ -23,7 +28,14 @@
:changed="isChanged"
:onclick="reset"
/>
<ServerSideIndicator :server-side="isServerSide" />
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
class="setting-description"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</label>
</template>

View file

@ -0,0 +1,88 @@
<!-- this is a helper exclusive to Setting components -->
<!-- TODO make it reusable -->
<template>
<span
class="DraftButtons"
>
<Popover
v-if="$parent.isDirty"
trigger="hover"
normal-button
:trigger-attrs="{ 'aria-label': $t('settings.commit_value_tooltip') }"
@click="$parent.commitDraft"
>
<template #trigger>
{{ $t('settings.commit_value') }}
</template>
<template #content>
<div class="modified-tooltip">
{{ $t('settings.commit_value_tooltip') }}
</div>
</template>
</Popover>
<Popover
v-if="$parent.isDirty"
trigger="hover"
normal-button
:trigger-attrs="{ 'aria-label': $t('settings.reset_value_tooltip') }"
@click="$parent.reset"
>
<template #trigger>
{{ $t('settings.reset_value') }}
</template>
<template #content>
<div class="modified-tooltip">
{{ $t('settings.reset_value_tooltip') }}
</div>
</template>
</Popover>
<Popover
v-if="$parent.canHardReset"
trigger="hover"
normal-button
:trigger-attrs="{ 'aria-label': $t('settings.hard_reset_value_tooltip') }"
@click="$parent.hardReset"
>
<template #trigger>
{{ $t('settings.hard_reset_value') }}
</template>
<template #content>
<div class="modified-tooltip">
{{ $t('settings.hard_reset_value_tooltip') }}
</div>
</template>
</Popover>
</span>
</template>
<script>
import Popover from 'src/components/popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faWrench } from '@fortawesome/free-solid-svg-icons'
library.add(
faWrench
)
export default {
components: { Popover },
props: ['changed']
}
</script>
<style lang="scss">
.DraftButtons {
display: inline-block;
position: relative;
.button-default {
margin-left: 0.5em;
}
}
.draft-tooltip {
margin: 0.5em 1em;
min-width: 10em;
text-align: center;
}
</style>

View file

@ -0,0 +1,16 @@
<template>
<NumberSetting
v-bind="$attrs"
>
<slot />
</NumberSetting>
</template>
<script>
import NumberSetting from './number_setting.vue'
export default {
components: {
NumberSetting
}
}
</script>

View file

@ -0,0 +1,13 @@
import { isEqual } from 'lodash'
import Setting from './setting.js'
export default {
...Setting,
computed: {
...Setting.computed,
isDirty () {
return !isEqual(this.state, this.draft)
}
}
}

View file

@ -0,0 +1,15 @@
<template>
<span
v-if="matchesExpertLevel"
class="GroupSetting"
>
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
</span>
</template>
<script src="./group_setting.js"></script>

View file

@ -1,44 +0,0 @@
import { get, set } from 'lodash'
import ModifiedIndicator from './modified_indicator.vue'
export default {
components: {
ModifiedIndicator
},
props: {
path: String,
disabled: Boolean,
min: Number,
expert: [Number, String]
},
computed: {
pathDefault () {
const [firstSegment, ...rest] = this.path.split('.')
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
state () {
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 this.state !== this.defaultState
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$parent.expertLevel
}
},
methods: {
update (e) {
set(this.$parent, this.path, parseInt(e.target.value))
},
reset () {
set(this.$parent, this.path, this.defaultState)
}
}
}

View file

@ -1,27 +1,17 @@
<template>
<span
v-if="matchesExpertLevel"
class="IntegerSetting"
<NumberSetting
v-bind="$attrs"
truncate="1"
>
<label :for="path">
<slot />
</label>
<input
:id="path"
class="number-input"
type="number"
step="1"
:disabled="disabled"
:min="min || 0"
:value="state"
@change="update"
>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
</span>
<slot />
</NumberSetting>
</template>
<script src="./integer_setting.js"></script>
<script>
import NumberSetting from './number_setting.vue'
export default {
components: {
NumberSetting
}
}
</script>

View file

@ -5,12 +5,12 @@
>
<Popover
trigger="hover"
:trigger-attrs="{ 'aria-label': $t('settings.setting_changed') }"
>
<template #trigger>
&nbsp;
<FAIcon
icon="wrench"
:aria-label="$t('settings.setting_changed')"
/>
</template>
<template #content>

View file

@ -0,0 +1,24 @@
import Setting from './setting.js'
export default {
...Setting,
props: {
...Setting.props,
truncate: {
type: Number,
required: false,
default: 1
}
},
methods: {
...Setting.methods,
getValue (e) {
if (!this.truncate === 1) {
return parseInt(e.target.value)
} else if (this.truncate > 1) {
return Math.trunc(e.target.value / this.truncate) * this.truncate
}
return parseFloat(e.target.value)
}
}
}

View file

@ -0,0 +1,45 @@
<template>
<span
v-if="matchesExpertLevel"
class="NumberSetting"
>
<label
:for="path"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</label>
<input
:id="path"
class="number-input"
type="number"
:step="step || 1"
:disabled="shouldBeDisabled"
:min="min || 0"
:value="realDraftMode ? draft :state"
@change="update"
>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</span>
</template>
<script src="./number_setting.js"></script>

View file

@ -1,7 +1,7 @@
<template>
<span
v-if="serverSide"
class="ServerSideIndicator"
v-if="isProfile"
class="ProfileSettingIndicator"
>
<Popover
trigger="hover"
@ -14,7 +14,7 @@
/>
</template>
<template #content>
<div class="serverside-tooltip">
<div class="profilesetting-tooltip">
{{ $t('settings.setting_server_side') }}
</div>
</template>
@ -33,17 +33,17 @@ library.add(
export default {
components: { Popover },
props: ['serverSide']
props: ['isProfile']
}
</script>
<style lang="scss">
.ServerSideIndicator {
.ProfileSettingIndicator {
display: inline-block;
position: relative;
}
.serverside-tooltip {
.profilesetting-tooltip {
margin: 0.5em 1em;
min-width: 10em;
text-align: center;

View file

@ -0,0 +1,237 @@
import ModifiedIndicator from './modified_indicator.vue'
import ProfileSettingIndicator from './profile_setting_indicator.vue'
import DraftButtons from './draft_buttons.vue'
import { get, set, cloneDeep } from 'lodash'
export default {
components: {
ModifiedIndicator,
DraftButtons,
ProfileSettingIndicator
},
props: {
path: {
type: [String, Array],
required: true
},
disabled: {
type: Boolean,
default: false
},
parentPath: {
type: [String, Array]
},
parentInvert: {
type: Boolean,
default: false
},
expert: {
type: [Number, String],
default: 0
},
source: {
type: String,
default: undefined
},
hideDescription: {
type: Boolean
},
swapDescriptionAndLabel: {
type: Boolean
},
overrideBackendDescription: {
type: Boolean
},
overrideBackendDescriptionLabel: {
type: Boolean
},
draftMode: {
type: Boolean,
default: undefined
}
},
inject: {
defaultSource: {
default: 'default'
},
defaultDraftMode: {
default: false
}
},
data () {
return {
localDraft: null
}
},
created () {
if (this.realDraftMode && this.realSource !== 'admin') {
this.draft = this.state
}
},
computed: {
draft: {
// TODO allow passing shared draft object?
get () {
if (this.realSource === 'admin') {
return get(this.$store.state.adminSettings.draft, this.canonPath)
} else {
return this.localDraft
}
},
set (value) {
if (this.realSource === 'admin') {
this.$store.commit('updateAdminDraft', { path: this.canonPath, value })
} else {
this.localDraft = value
}
}
},
state () {
const value = get(this.configSource, this.canonPath)
if (value === undefined) {
return this.defaultState
} else {
return value
}
},
visibleState () {
return this.realDraftMode ? this.draft : this.state
},
realSource () {
return this.source || this.defaultSource
},
realDraftMode () {
return typeof this.draftMode === 'undefined' ? this.defaultDraftMode : this.draftMode
},
backendDescription () {
return get(this.$store.state.adminSettings.descriptions, this.path)
},
backendDescriptionLabel () {
if (this.realSource !== 'admin') return ''
if (!this.backendDescription || this.overrideBackendDescriptionLabel) {
return this.$t([
'admin_dash',
'temp_overrides',
...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
'label'
].join('.'))
} else {
return this.swapDescriptionAndLabel
? this.backendDescription?.description
: this.backendDescription?.label
}
},
backendDescriptionDescription () {
if (this.realSource !== 'admin') return ''
if (this.hideDescription) return null
if (!this.backendDescription || this.overrideBackendDescription) {
return this.$t([
'admin_dash',
'temp_overrides',
...this.canonPath.map(p => p.replace(/\./g, '_DOT_')),
'description'
].join('.'))
} else {
return this.swapDescriptionAndLabel
? this.backendDescription?.label
: this.backendDescription?.description
}
},
backendDescriptionSuggestions () {
return this.backendDescription?.suggestions
},
shouldBeDisabled () {
const parentValue = this.parentPath !== undefined ? get(this.configSource, this.parentPath) : null
return this.disabled || (parentValue !== null ? (this.parentInvert ? parentValue : !parentValue) : false)
},
configSource () {
switch (this.realSource) {
case 'profile':
return this.$store.state.profileConfig
case 'admin':
return this.$store.state.adminSettings.config
default:
return this.$store.getters.mergedConfig
}
},
configSink () {
switch (this.realSource) {
case 'profile':
return (k, v) => this.$store.dispatch('setProfileOption', { name: k, value: v })
case 'admin':
return (k, v) => this.$store.dispatch('pushAdminSetting', { path: k, value: v })
default:
return (k, v) => this.$store.dispatch('setOption', { name: k, value: v })
}
},
defaultState () {
switch (this.realSource) {
case 'profile':
return {}
default:
return get(this.$store.getters.defaultConfig, this.path)
}
},
isProfileSetting () {
return this.realSource === 'profile'
},
isChanged () {
switch (this.realSource) {
case 'profile':
case 'admin':
return false
default:
return this.state !== this.defaultState
}
},
canonPath () {
return Array.isArray(this.path) ? this.path : this.path.split('.')
},
isDirty () {
if (this.realSource === 'admin' && this.canonPath.length > 3) {
return false // should not show draft buttons for "grouped" values
} else {
return this.realDraftMode && this.draft !== this.state
}
},
canHardReset () {
return this.realSource === 'admin' && this.$store.state.adminSettings.modifiedPaths.has(this.canonPath.join(' -> '))
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$store.state.config.expertLevel > 0
}
},
methods: {
getValue (e) {
return e.target.value
},
update (e) {
if (this.realDraftMode) {
this.draft = this.getValue(e)
} else {
this.configSink(this.path, this.getValue(e))
}
},
commitDraft () {
if (this.realDraftMode) {
this.configSink(this.path, this.draft)
}
},
reset () {
if (this.realDraftMode) {
this.draft = cloneDeep(this.state)
} else {
set(this.$store.getters.mergedConfig, this.path, cloneDeep(this.defaultState))
}
},
hardReset () {
switch (this.realSource) {
case 'admin':
return this.$store.dispatch('resetAdminSetting', { path: this.path })
.then(() => { this.draft = this.state })
default:
console.warn('Hard reset not implemented yet!')
}
}
}
}

View file

@ -1,52 +1,18 @@
import { defaultState as configDefaultState } from 'src/modules/config.js'
import { defaultState as serverSideConfigDefaultState } from 'src/modules/serverSideConfig.js'
const SharedComputedObject = () => ({
user () {
return this.$store.state.users.currentUser
},
// Getting values for default properties
...Object.keys(configDefaultState)
.map(key => [
key + 'DefaultValue',
function () {
return this.$store.getters.defaultConfig[key]
}
])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Generating computed values for vuex properties
...Object.keys(configDefaultState)
.map(key => [key, {
get () { return this.$store.getters.mergedConfig[key] },
set (value) {
this.$store.dispatch('setOption', { name: key, value })
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
...Object.keys(serverSideConfigDefaultState)
.map(key => ['serverSide_' + key, {
get () { return this.$store.state.serverSideConfig[key] },
set (value) {
this.$store.dispatch('setServerSideOption', { name: key, value })
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Special cases (need to transform values or perform actions first)
useStreamingApi: {
get () { return this.$store.getters.mergedConfig.useStreamingApi },
set (value) {
const promise = value
? this.$store.dispatch('enableMastoSockets')
: this.$store.dispatch('disableMastoSockets')
promise.then(() => {
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
}).catch((e) => {
console.error('Failed starting MastoAPI Streaming socket', e)
this.$store.dispatch('disableMastoSockets')
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
})
}
expertLevel () {
return this.$store.getters.mergedConfig.expertLevel > 0
},
mergedConfig () {
return this.$store.getters.mergedConfig
},
adminConfig () {
return this.$store.state.adminSettings.config
},
adminDraft () {
return this.$store.state.adminSettings.draft
}
})

View file

@ -1,67 +1,40 @@
import { get, set } from 'lodash'
import ModifiedIndicator from './modified_indicator.vue'
import Select from 'src/components/select/select.vue'
import Setting from './setting.js'
export const allCssUnits = ['cm', 'mm', 'in', 'px', 'pt', 'pc', 'em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', '%']
export const defaultHorizontalUnits = ['px', 'rem', 'vw']
export const defaultVerticalUnits = ['px', 'rem', 'vh']
export default {
...Setting,
components: {
ModifiedIndicator,
...Setting.components,
Select
},
props: {
path: String,
disabled: Boolean,
...Setting.props,
min: Number,
units: {
type: [String],
type: Array,
default: () => allCssUnits
},
expert: [Number, String]
}
},
computed: {
pathDefault () {
const [firstSegment, ...rest] = this.path.split('.')
return [firstSegment + 'DefaultValue', ...rest].join('.')
},
...Setting.computed,
stateUnit () {
return (this.state || '').replace(/\d+/, '')
return this.state.replace(/\d+/, '')
},
stateValue () {
return (this.state || '').replace(/\D+/, '')
},
state () {
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 this.state !== this.defaultState
},
matchesExpertLevel () {
return (this.expert || 0) <= this.$parent.expertLevel
return this.state.replace(/\D+/, '')
}
},
methods: {
update (e) {
set(this.$parent, this.path, e)
},
reset () {
set(this.$parent, this.path, this.defaultState)
},
...Setting.methods,
updateValue (e) {
set(this.$parent, this.path, parseInt(e.target.value) + this.stateUnit)
this.configSink(this.path, parseInt(e.target.value) + this.stateUnit)
},
updateUnit (e) {
set(this.$parent, this.path, this.stateValue + e.target.value)
this.configSink(this.path, this.stateValue + e.target.value)
}
}
}

View file

@ -45,11 +45,18 @@
<script src="./size_setting.js"></script>
<style lang="scss">
.css-unit-input,
.css-unit-input select {
margin-left: 0.5em;
width: 4em;
max-width: 4em;
min-width: 4em;
.SizeSetting {
.number-input {
max-width: 6.5em;
}
.css-unit-input,
.css-unit-input select {
margin-left: 0.5em;
width: 4em;
max-width: 4em;
min-width: 4em;
}
}
</style>

View file

@ -0,0 +1,5 @@
import Setting from './setting.js'
export default {
...Setting
}

View file

@ -0,0 +1,42 @@
<template>
<label
v-if="matchesExpertLevel"
class="StringSetting"
>
<label
:for="path"
:class="{ 'faint': shouldBeDisabled }"
>
<template v-if="backendDescriptionLabel">
{{ backendDescriptionLabel + ' ' }}
</template>
<template v-else-if="source === 'admin'">
MISSING LABEL FOR {{ path }}
</template>
<slot v-else />
</label>
<input
:id="path"
class="string-input"
:disabled="shouldBeDisabled"
:value="realDraftMode ? draft : state"
@change="update"
>
{{ ' ' }}
<ModifiedIndicator
:changed="isChanged"
:onclick="reset"
/>
<ProfileSettingIndicator :is-profile="isProfileSetting" />
<DraftButtons />
<p
v-if="backendDescriptionDescription"
class="setting-description"
:class="{ 'faint': shouldBeDisabled }"
>
{{ backendDescriptionDescription + ' ' }}
</p>
</label>
</template>
<script src="./string_setting.js"></script>

View file

@ -5,7 +5,7 @@ import getResettableAsyncComponent from 'src/services/resettable_async_component
import Popover from '../popover/popover.vue'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { cloneDeep } from 'lodash'
import { cloneDeep, isEqual } from 'lodash'
import {
newImporter,
newExporter
@ -53,8 +53,16 @@ const SettingsModal = {
Modal,
Popover,
Checkbox,
SettingsModalContent: getResettableAsyncComponent(
() => import('./settings_modal_content.vue'),
SettingsModalUserContent: getResettableAsyncComponent(
() => import('./settings_modal_user_content.vue'),
{
loadingComponent: PanelLoading,
errorComponent: AsyncComponentError,
delay: 0
}
),
SettingsModalAdminContent: getResettableAsyncComponent(
() => import('./settings_modal_admin_content.vue'),
{
loadingComponent: PanelLoading,
errorComponent: AsyncComponentError,
@ -147,6 +155,12 @@ const SettingsModal = {
PLEROMAFE_SETTINGS_MINOR_VERSION
]
return clone
},
resetAdminDraft () {
this.$store.commit('resetAdminDraft')
},
pushAdminDraft () {
this.$store.dispatch('pushAdminDraft')
}
},
computed: {
@ -156,8 +170,14 @@ const SettingsModal = {
modalActivated () {
return this.$store.state.interface.settingsModalState !== 'hidden'
},
modalOpenedOnce () {
return this.$store.state.interface.settingsModalLoaded
modalMode () {
return this.$store.state.interface.settingsModalMode
},
modalOpenedOnceUser () {
return this.$store.state.interface.settingsModalLoadedUser
},
modalOpenedOnceAdmin () {
return this.$store.state.interface.settingsModalLoadedAdmin
},
modalPeeked () {
return this.$store.state.interface.settingsModalState === 'minimized'
@ -167,9 +187,14 @@ const SettingsModal = {
return this.$store.state.config.expertLevel > 0
},
set (value) {
console.log(value)
this.$store.dispatch('setOption', { name: 'expertLevel', value: value ? 1 : 0 })
}
},
adminDraftAny () {
return !isEqual(
this.$store.state.adminSettings.config,
this.$store.state.adminSettings.draft
)
}
}
}

View file

@ -17,6 +17,12 @@
}
}
.setting-description {
margin-top: 0.2em;
margin-bottom: 2em;
font-size: 70%;
}
.settings-modal-panel {
overflow: hidden;
transition: transform;
@ -37,7 +43,9 @@
.btn {
min-height: 2em;
min-width: 10em;
}
.btn:not(.dropdown-button) {
padding: 0 2em;
}
}
@ -45,6 +53,8 @@
.settings-footer {
display: flex;
flex-wrap: wrap;
line-height: 2;
>* {
margin-right: 0.5em;

View file

@ -8,7 +8,7 @@
<div class="settings-modal-panel panel">
<div class="panel-heading">
<span class="title">
{{ $t('settings.settings') }}
{{ modalMode === 'user' ? $t('settings.settings') : $t('admin_dash.window_title') }}
</span>
<transition name="fade">
<div
@ -42,10 +42,12 @@
</button>
</div>
<div class="panel-body">
<SettingsModalContent v-if="modalOpenedOnce" />
<SettingsModalUserContent v-if="modalMode === 'user' && modalOpenedOnceUser" />
<SettingsModalAdminContent v-if="modalMode === 'admin' && modalOpenedOnceAdmin" />
</div>
<div class="panel-footer settings-footer">
<div class="panel-footer settings-footer -flexible-height">
<Popover
v-if="modalMode === 'user'"
class="export"
trigger="click"
placement="top"
@ -107,10 +109,42 @@
>
{{ $t("settings.expert_mode") }}
</Checkbox>
<span v-if="modalMode === 'admin'">
<i18n-t keypath="admin_dash.wip_notice">
<template #adminFeLink>
<a
href="/pleroma/admin/#/login-pleroma"
target="_blank"
>
{{ $t("admin_dash.old_ui_link") }}
</a>
</template>
</i18n-t>
</span>
<span
id="unscrolled-content"
class="extra-content"
/>
<span
v-if="modalMode === 'admin'"
class="admin-buttons"
>
<button
class="button-default btn"
:disabled="!adminDraftAny"
@click="resetAdminDraft"
>
{{ $t("admin_dash.reset_all") }}
</button>
{{ ' ' }}
<button
class="button-default btn"
:disabled="!adminDraftAny"
@click="pushAdminDraft"
>
{{ $t("admin_dash.commit_all") }}
</button>
</span>
</div>
</div>
</Modal>

View file

@ -0,0 +1,93 @@
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import InstanceTab from './admin_tabs/instance_tab.vue'
import LimitsTab from './admin_tabs/limits_tab.vue'
import FrontendsTab from './admin_tabs/frontends_tab.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faWrench,
faHand,
faLaptopCode,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
} from '@fortawesome/free-solid-svg-icons'
library.add(
faWrench,
faHand,
faLaptopCode,
faPaintBrush,
faBell,
faDownload,
faEyeSlash,
faInfo
)
const SettingsModalAdminContent = {
components: {
TabSwitcher,
InstanceTab,
LimitsTab,
FrontendsTab
},
computed: {
user () {
return this.$store.state.users.currentUser
},
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
open () {
return this.$store.state.interface.settingsModalState !== 'hidden'
},
bodyLock () {
return this.$store.state.interface.settingsModalState === 'visible'
},
adminDbLoaded () {
return this.$store.state.adminSettings.loaded
},
adminDescriptionsLoaded () {
return this.$store.state.adminSettings.descriptions !== null
},
noDb () {
return this.$store.state.adminSettings.dbConfigEnabled === false
}
},
created () {
if (this.user.rights.admin) {
this.$store.dispatch('loadAdminStuff')
}
},
methods: {
onOpen () {
const targetTab = this.$store.state.interface.settingsModalTargetTab
// We're being told to open in specific tab
if (targetTab) {
const tabIndex = this.$refs.tabSwitcher.$slots.default().findIndex(elm => {
return elm.props && elm.props['data-tab-name'] === targetTab
})
if (tabIndex >= 0) {
this.$refs.tabSwitcher.setTab(tabIndex)
}
}
// Clear the state of target tab, so that next time settings is opened
// it doesn't force it.
this.$store.dispatch('clearSettingsModalTargetTab')
}
},
mounted () {
this.onOpen()
},
watch: {
open: function (value) {
if (value) this.onOpen()
}
}
}
export default SettingsModalAdminContent

View file

@ -48,9 +48,5 @@
color: var(--cRed, $fallback--cRed);
color: $fallback--cRed;
}
.number-input {
max-width: 6em;
}
}
}

View file

@ -0,0 +1,68 @@
<template>
<tab-switcher
v-if="adminDescriptionsLoaded && (noDb || adminDbLoaded)"
ref="tabSwitcher"
class="settings_tab-switcher"
:side-tab-bar="true"
:scrollable-tabs="true"
:render-only-focused="true"
:body-scroll-lock="bodyLock"
>
<div
v-if="noDb"
:label="$t('admin_dash.tabs.nodb')"
icon="exclamation-triangle"
data-tab-name="nodb-notice"
>
<div :label="$t('admin_dash.tabs.nodb')">
<div class="setting-item">
<h2>{{ $t('admin_dash.nodb.heading') }}</h2>
<i18n-t keypath="admin_dash.nodb.text">
<template #documentation>
<a
href="https://docs-develop.pleroma.social/backend/configuration/howto_database_config/"
target="_blank"
>
{{ $t("admin_dash.nodb.documentation") }}
</a>
</template>
<template #property>
<code>config :pleroma, configurable_from_database</code>
</template>
<template #value>
<code>true</code>
</template>
</i18n-t>
<p>{{ $t('admin_dash.nodb.text2') }}</p>
</div>
</div>
</div>
<div
v-if="adminDbLoaded"
:label="$t('admin_dash.tabs.instance')"
icon="wrench"
data-tab-name="general"
>
<InstanceTab />
</div>
<div
v-if="adminDbLoaded"
:label="$t('admin_dash.tabs.limits')"
icon="hand"
data-tab-name="limits"
>
<LimitsTab />
</div>
<div
:label="$t('admin_dash.tabs.frontends')"
icon="laptop-code"
data-tab-name="frontends"
>
<FrontendsTab />
</div>
</tab-switcher>
</template>
<script src="./settings_modal_admin_content.js"></script>
<style src="./settings_modal_admin_content.scss" lang="scss"></style>

View file

@ -0,0 +1,52 @@
@import "src/variables";
.settings_tab-switcher {
height: 100%;
.setting-item {
border-bottom: 2px solid var(--fg, $fallback--fg);
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div,
> label {
display: block;
margin-bottom: 0.5em;
&:last-child {
margin-bottom: 0;
}
}
.select-multiple {
display: flex;
.option-list {
margin: 0;
padding-left: 0.5em;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
select {
min-width: 10em;
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
.unavailable,
.unavailable svg {
color: var(--cRed, $fallback--cRed);
color: $fallback--cRed;
}
}
}

View file

@ -78,6 +78,6 @@
</tab-switcher>
</template>
<script src="./settings_modal_content.js"></script>
<script src="./settings_modal_user_content.js"></script>
<style src="./settings_modal_content.scss" lang="scss"></style>
<style src="./settings_modal_user_content.scss" lang="scss"></style>

View file

@ -7,13 +7,11 @@
<BooleanSetting path="hideFilteredStatuses">
{{ $t('settings.hide_filtered_statuses') }}
</BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
:disabled="hideFilteredStatuses"
parent-path="hideFilteredStatuses"
:parent-invert="true"
path="hideWordFilteredPosts"
>
{{ $t('settings.hide_wordfiltered_statuses') }}
@ -22,7 +20,8 @@
<li>
<BooleanSetting
v-if="user"
:disabled="hideFilteredStatuses"
parent-path="hideFilteredStatuses"
:parent-invert="true"
path="hideMutedThreads"
>
{{ $t('settings.hide_muted_threads') }}
@ -31,7 +30,8 @@
<li>
<BooleanSetting
v-if="user"
:disabled="hideFilteredStatuses"
parent-path="hideFilteredStatuses"
:parent-invert="true"
path="hideMutedPosts"
>
{{ $t('settings.hide_muted_posts') }}

View file

@ -2,11 +2,12 @@ import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import ScopeSelector from 'src/components/scope_selector/scope_selector.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import SizeSetting, { defaultHorizontalUnits } from '../helpers/size_setting.vue'
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import ServerSideIndicator from '../helpers/server_side_indicator.vue'
import ProfileSettingIndicator from '../helpers/profile_setting_indicator.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faGlobe
@ -62,10 +63,11 @@ const GeneralTab = {
BooleanSetting,
ChoiceSetting,
IntegerSetting,
FloatSetting,
SizeSetting,
InterfaceLanguageSwitcher,
ScopeSelector,
ServerSideIndicator
ProfileSettingIndicator
},
computed: {
horizontalUnits () {
@ -108,7 +110,7 @@ const GeneralTab = {
},
methods: {
changeDefaultScope (value) {
this.$store.dispatch('setServerSideOption', { name: 'defaultScope', value })
this.$store.dispatch('setProfileOption', { name: 'defaultScope', value })
}
}
}

View file

@ -29,14 +29,11 @@
<BooleanSetting path="streaming">
{{ $t('settings.streaming') }}
</BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="pauseOnUnfocused"
:disabled="!streaming"
parent-path="streaming"
>
{{ $t('settings.pause_on_unfocused') }}
</BooleanSetting>
@ -213,7 +210,7 @@
</ChoiceSetting>
</li>
<ul
v-if="conversationDisplay !== 'linear'"
v-if="mergedConfig.conversationDisplay !== 'linear'"
class="setting-list suboptions"
>
<li>
@ -265,12 +262,22 @@
<li>
<BooleanSetting
v-if="user"
path="serverSide_stripRichContent"
source="profile"
path="stripRichContent"
expert="1"
>
{{ $t('settings.no_rich_text_description') }}
</BooleanSetting>
</li>
<li>
<FloatSetting
v-if="user"
path="emojiReactionsScale"
expert="1"
>
{{ $t('settings.emoji_reactions_scale') }}
</FloatSetting>
</li>
<h3>{{ $t('settings.attachments') }}</h3>
<li>
<BooleanSetting
@ -290,7 +297,7 @@
<BooleanSetting
path="preloadImage"
expert="1"
:disabled="!hideNsfw"
parent-path="hideNsfw"
>
{{ $t('settings.preload_images') }}
</BooleanSetting>
@ -299,7 +306,7 @@
<BooleanSetting
path="useOneClickNsfw"
expert="1"
:disabled="!hideNsfw"
parent-path="hideNsfw"
>
{{ $t('settings.use_one_click_nsfw') }}
</BooleanSetting>
@ -312,15 +319,13 @@
>
{{ $t('settings.loop_video') }}
</BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !streaming}]"
>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="loopVideoSilentOnly"
expert="1"
:disabled="!loopVideo || !loopSilentAvailable"
parent-path="loopVideo"
:disabled="!loopSilentAvailable"
>
{{ $t('settings.loop_video_silent_only') }}
</BooleanSetting>
@ -418,18 +423,18 @@
<ul class="setting-list">
<li>
<label for="default-vis">
{{ $t('settings.default_vis') }} <ServerSideIndicator :server-side="true" />
{{ $t('settings.default_vis') }} <ProfileSettingIndicator :is-profile="true" />
<ScopeSelector
class="scope-selector"
:show-all="true"
:user-default="serverSide_defaultScope"
:initial-scope="serverSide_defaultScope"
:user-default="$store.state.profileConfig.defaultScope"
:initial-scope="$store.state.profileConfig.defaultScope"
:on-scope-change="changeDefaultScope"
/>
</label>
</li>
<li>
<!-- <BooleanSetting path="serverSide_defaultNSFW"> -->
<!-- <BooleanSetting source="profile" path="defaultNSFW"> -->
<BooleanSetting path="sensitiveByDefault">
{{ $t('settings.sensitive_by_default') }}
</BooleanSetting>
@ -501,6 +506,14 @@
{{ $t('settings.pad_emoji') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting
path="autocompleteSelect"
expert="1"
>
{{ $t('settings.autocomplete_select_first') }}
</BooleanSetting>
</li>
</ul>
</div>
</div>

View file

@ -9,17 +9,20 @@ import DomainMuteCard from 'src/components/domain_mute_card/domain_mute_card.vue
import SelectableList from 'src/components/selectable_list/selectable_list.vue'
import ProgressButton from 'src/components/progress_button/progress_button.vue'
import withSubscription from 'src/components/../hocs/with_subscription/with_subscription'
import withLoadMore from 'src/components/../hocs/with_load_more/with_load_more'
import Checkbox from 'src/components/checkbox/checkbox.vue'
const BlockList = withSubscription({
const BlockList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
destroy: () => {},
childPropName: 'items'
})(SelectableList)
const MuteList = withSubscription({
const MuteList = withLoadMore({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
destroy: () => {},
childPropName: 'items'
})(SelectableList)

View file

@ -4,7 +4,10 @@
<h2>{{ $t('settings.notification_setting_filters') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting path="serverSide_blockNotificationsFromStrangers">
<BooleanSetting
source="profile"
path="blockNotificationsFromStrangers"
>
{{ $t('settings.notification_setting_block_from_strangers') }}
</BooleanSetting>
</li>
@ -67,7 +70,8 @@
</li>
<li>
<BooleanSetting
path="serverSide_webPushHideContents"
source="profile"
path="webPushHideContents"
expert="1"
>
{{ $t('settings.notification_setting_hide_notification_contents') }}

View file

@ -12,6 +12,7 @@ import InterfaceLanguageSwitcher from 'src/components/interface_language_switche
import BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js'
import localeService from 'src/services/locale/locale.service.js'
import { propsToNative } from 'src/services/attributes_helper/attributes_helper.service.js'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
@ -261,6 +262,9 @@ const ProfileTab = {
messageArgs: [error.message],
level: 'error'
})
},
propsToNative (props) {
return propsToNative(props)
}
}
}

View file

@ -8,11 +8,14 @@
enable-emoji-picker
:suggest="emojiSuggestor"
>
<input
id="username"
v-model="newName"
class="name-changer"
>
<template #default="inputProps">
<input
id="username"
v-model="newName"
class="name-changer"
v-bind="propsToNative(inputProps)"
>
</template>
</EmojiInput>
<p>{{ $t('settings.bio') }}</p>
<EmojiInput
@ -20,10 +23,13 @@
enable-emoji-picker
:suggest="emojiUserSuggestor"
>
<textarea
v-model="newBio"
class="bio resize-height"
/>
<template #default="inputProps">
<textarea
v-model="newBio"
class="bio resize-height"
v-bind="propsToNative(inputProps)"
/>
</template>
</EmojiInput>
<p v-if="role === 'admin' || role === 'moderator'">
<Checkbox v-model="showRole">
@ -60,10 +66,13 @@
hide-emoji-button
:suggest="userSuggestor"
>
<input
v-model="newFields[i].name"
:placeholder="$t('settings.profile_fields.name')"
>
<template #default="inputProps">
<input
v-model="newFields[i].name"
:placeholder="$t('settings.profile_fields.name')"
v-bind="propsToNative(inputProps)"
>
</template>
</EmojiInput>
<EmojiInput
v-model="newFields[i].value"
@ -71,10 +80,13 @@
hide-emoji-button
:suggest="userSuggestor"
>
<input
v-model="newFields[i].value"
:placeholder="$t('settings.profile_fields.value')"
>
<template #default="inputProps">
<input
v-model="newFields[i].value"
:placeholder="$t('settings.profile_fields.value')"
v-bind="propsToNative(inputProps)"
>
</template>
</EmojiInput>
<button
class="delete-field button-unstyled -hover-highlight"
@ -242,37 +254,50 @@
<h2>{{ $t('settings.account_privacy') }}</h2>
<ul class="setting-list">
<li>
<BooleanSetting path="serverSide_locked">
<BooleanSetting
source="profile"
path="locked"
>
{{ $t('settings.lock_account_description') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="serverSide_discoverable">
<BooleanSetting
source="profile"
path="discoverable"
>
{{ $t('settings.discoverable') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="serverSide_allowFollowingMove">
<BooleanSetting
source="profile"
path="allowFollowingMove"
>
{{ $t('settings.allow_following_move') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="serverSide_hideFavorites">
<BooleanSetting
source="profile"
path="hideFavorites"
>
{{ $t('settings.hide_favorites_description') }}
</BooleanSetting>
</li>
<li>
<BooleanSetting path="serverSide_hideFollowers">
<BooleanSetting
source="profile"
path="hideFollowers"
>
{{ $t('settings.hide_followers_description') }}
</BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !serverSide_hideFollowers}]"
>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="serverSide_hideFollowersCount"
:disabled="!serverSide_hideFollowers"
source="profile"
path="hideFollowersCount"
parent-path="hideFollowers"
>
{{ $t('settings.hide_followers_count_description') }}
</BooleanSetting>
@ -280,17 +305,18 @@
</ul>
</li>
<li>
<BooleanSetting path="serverSide_hideFollows">
<BooleanSetting
source="profile"
path="hideFollows"
>
{{ $t('settings.hide_follows_description') }}
</BooleanSetting>
<ul
class="setting-list suboptions"
:class="[{disabled: !serverSide_hideFollows}]"
>
<ul class="setting-list suboptions">
<li>
<BooleanSetting
path="serverSide_hideFollowsCount"
:disabled="!serverSide_hideFollows"
source="profile"
path="hideFollowsCount"
parent-path="hideFollows"
>
{{ $t('settings.hide_follows_count_description') }}
</BooleanSetting>

View file

@ -143,8 +143,8 @@
/>
</div>
<div>
<i18n
path="settings.new_alias_target"
<i18n-t
keypath="settings.new_alias_target"
tag="p"
>
<code
@ -152,7 +152,7 @@
>
foo@example.org
</code>
</i18n>
</i18n-t>
<input
v-model="addAliasTarget"
>
@ -175,16 +175,16 @@
<h2>{{ $t('settings.move_account') }}</h2>
<p>{{ $t('settings.move_account_notes') }}</p>
<div>
<i18n
path="settings.move_account_target"
<i18n-t
keypath="settings.move_account_target"
tag="p"
>
<code
place="example"
>
foo@example.org
</code>
</i18n>
<template #example>
<code>
foo@example.org
</code>
</template>
</i18n-t>
<input
v-model="moveAccountTarget"
>

View file

@ -129,12 +129,13 @@
v-model="selected.inset"
:disabled="!present"
name="inset"
class="input-inset"
class="input-inset visible-for-screenreader-only"
type="checkbox"
>
<label
class="checkbox-label"
for="inset"
:aria-hidden="true"
/>
</div>
<div

View file

@ -115,7 +115,10 @@ const SideDrawer = {
GestureService.updateSwipe(e, this.closeGesture)
},
openSettingsModal () {
this.$store.dispatch('openSettingsModal')
this.$store.dispatch('openSettingsModal', 'user')
},
openAdminModal () {
this.$store.dispatch('openSettingsModal', 'admin')
}
}
}

View file

@ -180,16 +180,16 @@
v-if="currentUser && currentUser.role === 'admin'"
@click="toggleDrawer"
>
<a
href="/pleroma/admin/#/login-pleroma"
target="_blank"
<button
class="button-unstyled -link -fullwidth"
@click.stop="openAdminModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="tachometer-alt"
/> {{ $t("nav.administration") }}
</a>
</button>
</li>
<li
v-if="currentUser && supportsAnnouncements"

View file

@ -60,13 +60,7 @@ export default {
const isWanted = slot => slot.props && slot.props['data-tab-name'] === tabName
return this.$slots.default().findIndex(isWanted) === this.activeIndex
}
},
settingsModalVisible () {
return this.settingsModalState === 'visible'
},
...mapState({
settingsModalState: state => state.interface.settingsModalState
})
}
},
beforeUpdate () {
const currentSlot = this.slots()[this.active]
@ -117,6 +111,7 @@ export default {
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
>
<img src={props.image} title={props['image-tooltip']}/>
{props.label ? '' : props.label}
@ -131,6 +126,7 @@ export default {
onClick={this.clickTab(index)}
class={classesTab.join(' ')}
type="button"
role="tab"
>
{!props.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={props.icon}/>)}
<span class="text">
@ -167,11 +163,15 @@ export default {
return (
<div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
<div class="tabs">
<div
class="tabs"
role="tablist"
>
{tabs}
</div>
<div
ref="contents"
role="tabpanel"
class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}
v-body-scroll-lock={this.bodyScrollLock}
>

View file

@ -98,7 +98,7 @@ const withLoadMore = ({
</button>
}
{!this.error && this.loading && <FAIcon spin icon="circle-notch"/>}
{!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries}>{this.$t('general.more')}</a>}
{!this.error && !this.loading && !this.bottomedOut && <a onClick={this.fetchEntries} role="button" tabindex="0">{this.$t('general.more')}</a>}
</div>
</div>
)

View file

@ -9,7 +9,8 @@
"scope_options": "",
"text_limit": "الحد الأقصى للنص",
"title": "الميّزات",
"who_to_follow": "للمتابعة"
"who_to_follow": "للمتابعة",
"upload_limit": "حد الرفع"
},
"finder": {
"error_fetching_user": "خطأ أثناء جلب صفحة المستخدم",
@ -17,7 +18,35 @@
},
"general": {
"apply": "تطبيق",
"submit": "إرسال"
"submit": "إرسال",
"error_retry": "حاول مجددًا",
"retry": "حاول مجدداً",
"optional": "اختياري",
"show_more": "اعرض المزيد",
"show_less": "اعرض أقل",
"cancel": "ألغ",
"disable": "عطّل",
"enable": "فعّل",
"confirm": "تأكيد",
"close": "أغلق",
"role": {
"admin": "مدير",
"moderator": "مشرف"
},
"generic_error_message": "حدث خطأ: {0}",
"never_show_again": "لا تظهره مجددًا",
"yes": "نعم",
"no": "لا",
"unpin": "ألغ تثبيت العنصر",
"undo": "تراجع",
"more": "المزيد",
"loading": "يحمل…",
"generic_error": "حدث خطأ",
"scope_in_timeline": {
"private": "المتابِعون فقط"
},
"scroll_to_top": "مرر لأعلى",
"pin": "ثبت العنصر"
},
"login": {
"login": "تسجيل الدخول",
@ -25,7 +54,19 @@
"password": "الكلمة السرية",
"placeholder": "مثال lain",
"register": "انشاء حساب",
"username": "إسم المستخدم"
"username": "إسم المستخدم",
"logout_confirm_title": "تأكيد الخروج",
"logout_confirm": "أتريد الخروج؟",
"logout_confirm_accept_button": "خروج",
"logout_confirm_cancel_button": "لا تخرج",
"hint": "لِج للانضمام للمناقشة",
"authentication_code": "رمز الاستيثاق",
"enter_recovery_code": "أدخل رمز التأكيد",
"enter_two_factor_code": "أدخل رمز الاستيثاق بعاملين",
"recovery_code": "رمز الاستعادة",
"heading": {
"totp": "الاستيثاق بعاملين"
}
},
"nav": {
"chat": "الدردشة المحلية",
@ -33,23 +74,48 @@
"mentions": "الإشارات",
"public_tl": "الخيط الزمني العام",
"timeline": "الخيط الزمني",
"twkn": "كافة الشبكة المعروفة"
"twkn": "كافة الشبكة المعروفة",
"search_close": "أغلق شربط البحث",
"back": "للخلف",
"administration": "الإدارة",
"preferences": "التفضيلات",
"chats": "المحادثات",
"lists": "القوائم",
"edit_nav_mobile": "خصص شريط التنقل",
"edit_pinned": "حرر العناصر المثبتة",
"mobile_notifications_close": "أغلق الاشعارات",
"announcements": "إعلانات",
"home_timeline": "الخط الزمني الرئيس",
"search": "بحث",
"who_to_follow": "للمتابعة",
"dms": "رسالة شخصية",
"edit_finish": "تم التحرير",
"timelines": "الخيوط الزمنية",
"mobile_notifications": "افتح الإشعارات (تتواجد اشعارات غير مقروءة)"
},
"notifications": {
"broken_favorite": "منشور مجهول، جارٍ البحث عنه…",
"favorited_you": "أعجِب بمنشورك",
"followed_you": "يُتابعك",
"load_older": "تحميل الإشعارات الأقدم",
"notifications": "الإخطارات",
"notifications": "الاشعارات",
"read": "مقروء!",
"repeated_you": "شارَك منشورك"
"repeated_you": "شارَك منشورك",
"error": "خطأ أثناء جلب الاشعارات: {0}",
"follow_request": "يريد متابعتك",
"poll_ended": "انتهى الاستطلاع",
"no_more_notifications": "لا مزيد من الإشعارات",
"reacted_with": "تفاعل بـ{0}",
"submitted_report": "أرسل بلاغًا"
},
"post_status": {
"account_not_locked_warning": "",
"account_not_locked_warning_link": "مقفل",
"attachments_sensitive": "اعتبر المرفقات كلها كمحتوى حساس",
"content_type": {
"text/plain": "نص صافٍ"
"text/plain": "نص صِرف",
"text/html": "HTML",
"text/markdown": "ماركداون"
},
"content_warning": "الموضوع (اختياري)",
"default": "وصلت للتوّ إلى لوس أنجلس.",
@ -60,15 +126,47 @@
"private": "",
"public": "علني - يُنشر على الخيوط الزمنية العمومية",
"unlisted": "غير مُدرَج - لا يُنشَر على الخيوط الزمنية العمومية"
}
},
"media_description": "وصف الوسائط",
"direct_warning_to_all": "سيكون عذا المنشور مرئيًا لكل المستخدمين المذكورين.",
"post": "انشر",
"preview": "معاينة",
"preview_empty": "فارغ",
"scope_notice": {
"public": "سيكون هذا المنشور مرئيًا للجميع",
"private": "سيكون هذا المنشور مرئيا لمتابِعيك فقط"
},
"direct_warning_to_first_only": "سيكون عذا المنشور مرئيًا للمستخدمين المذكورين في أول الرسالة.",
"edit_unsupported_warning": "بليروما لا يدعم تعديل الذكر والاستطلاع.",
"empty_status_error": "يتعذر نشر منشور فارغ دون ملفات"
},
"registration": {
"bio": "السيرة الذاتية",
"email": "عنوان البريد الإلكتروني",
"fullname": "الإسم المعروض",
"fullname": "الاسم العلني",
"password_confirm": "تأكيد الكلمة السرية",
"registration": "التسجيل",
"token": "رمز الدعوة"
"token": "رمز الدعوة",
"bio_optional": "سيرة (اختيارية)",
"email_optional": "بيرد إلكتروني (اختياري)",
"username_placeholder": "مثل lain",
"reason": "سبب التسجيل",
"register": "سجل",
"validations": {
"username_required": "لايمكن تركه فارغًا",
"email_required": "لايمكن تركه فارغًا",
"password_required": "لايمكن تركه فارغًا",
"password_confirmation_required": "لايمكن تركه فارغًا",
"fullname_required": "لايمكن تركه فارغًا",
"password_confirmation_match": "يلزم أن يطابق كلمة السر",
"birthday_required": "لايمكن تركه فارغًا",
"birthday_min_age": "يلزم أن يكون في {date} أو قبله"
},
"fullname_placeholder": "مثل Lain Iwakura",
"reason_placeholder": "قبول التسجيل في هذا المثيل يستلزم موافقة المدير\nلهذا يجب عليك إعلامه بسبب التسجيل.",
"birthday_optional": "تاريخ الميلاد (اختياري):",
"email_language": "بأي لغة تريد استلام رسائل البريد الإلكتروني؟",
"birthday": "تاريخ الميلاد:"
},
"settings": {
"attachmentRadius": "المُرفَقات",
@ -83,9 +181,9 @@
"cGreen": "أخضر (إعادة النشر)",
"cOrange": "برتقالي (مفضلة)",
"cRed": "أحمر (إلغاء)",
"change_password": "تغيير كلمة السر",
"change_password_error": "وقع هناك خلل أثناء تعديل كلمتك السرية.",
"changed_password": "تم تغيير كلمة المرور بنجاح!",
"change_password": "غيّر كلمة السر",
"change_password_error": "حدث خلل أثناء تعديل كلمتك السرية.",
"changed_password": "نجح تغيير كلمة السر!",
"collapse_subject": "",
"confirm_new_password": "تأكيد كلمة السر الجديدة",
"current_avatar": "صورتك الرمزية الحالية",
@ -94,11 +192,11 @@
"data_import_export_tab": "تصدير واستيراد البيانات",
"default_vis": "أسلوب العرض الافتراضي",
"delete_account": "حذف الحساب",
"delete_account_description": "حذف حسابك و كافة منشوراتك نهائيًا.",
"delete_account_error": "",
"delete_account_description": "حذف حسابك و كافة بياناتك نهائيًا.",
"delete_account_error": "حدثة مشكلة اثناء حذف حسابك، إذا استمرت تواصل مع مدير المثيل.",
"delete_account_instructions": "يُرجى إدخال كلمتك السرية أدناه لتأكيد عملية حذف الحساب.",
"export_theme": "حفظ النموذج",
"filtering": "التصفية",
"filtering": "الترشيح",
"filtering_explanation": "سيتم إخفاء كافة المنشورات التي تحتوي على هذه الكلمات، كلمة واحدة في كل سطر",
"follow_export": "تصدير الاشتراكات",
"follow_export_button": "تصدير الاشتراكات كملف csv",
@ -108,30 +206,30 @@
"follows_imported": "",
"foreground": "الأمامية",
"general": "الإعدادات العامة",
"hide_attachments_in_convo": "إخفاء المرفقات على المحادثات",
"hide_attachments_in_tl": "إخفاء المرفقات على الخيط الزمني",
"hide_post_stats": "",
"hide_user_stats": "",
"import_followers_from_a_csv_file": "",
"hide_attachments_in_convo": "اخف المرفقات من المحادثات",
"hide_attachments_in_tl": "اخف المرفقات من الخيط الزمني",
"hide_post_stats": "اخف احصائيات المنشور (مثل عدد التفضيلات)",
"hide_user_stats": "اخف احصائيات المستخدم (مثل عدد المتابِعين)",
"import_followers_from_a_csv_file": "استورد المتابِعين من ملف csv",
"import_theme": "تحميل نموذج",
"inputRadius": "",
"instance_default": "",
"instance_default": "(الافتراضي: {value})",
"interfaceLanguage": "لغة الواجهة",
"invalid_theme_imported": "",
"invalid_theme_imported": "الملف المختار ليس سمة تدعمها بليروما.لن تطرأ تغييرات على سمتك.",
"limited_availability": "غير متوفر على متصفحك",
"links": "الروابط",
"lock_account_description": "",
"loop_video": "",
"loop_video_silent_only": "",
"loop_video": "كرر الفيديوهات",
"loop_video_silent_only": "كرر فيديوهات بدون صوت (مثل gif في ماستودون)",
"name": "الاسم",
"name_bio": "الاسم والسيرة الذاتية",
"new_password": "كلمة السر الجديدة",
"no_rich_text_description": "",
"notification_visibility": "نوع الإشعارات التي تريد عرضها",
"notification_visibility_follows": "يتابع",
"notification_visibility_likes": "الإعجابات",
"notification_visibility_mentions": "الإشارات",
"notification_visibility_repeats": "",
"notification_visibility_likes": "المفضلة",
"notification_visibility_mentions": "ذِكر",
"notification_visibility_repeats": "مشاركات",
"nsfw_clickthrough": "",
"oauth_tokens": "رموز OAuth",
"token": "رمز",
@ -141,16 +239,16 @@
"panelRadius": "",
"pause_on_unfocused": "",
"presets": "النماذج",
"profile_background": "خلفية الصفحة الشخصية",
"profile_background": "خلفية الملف التعريفي",
"profile_banner": "رأسية الصفحة الشخصية",
"profile_tab": "الملف الشخصي",
"profile_tab": "الملف التعريفي",
"radii_help": "",
"replies_in_timeline": "الردود على الخيط الزمني",
"reply_visibility_all": "عرض كافة الردود",
"replies_in_timeline": "المشاركات في الخيط الزمني",
"reply_visibility_all": "أظهر كل المشاركات",
"reply_visibility_following": "",
"reply_visibility_self": "",
"saving_err": "خطأ أثناء حفظ الإعدادات",
"saving_ok": "تم حفظ الإعدادات",
"saving_ok": "حُفظت الإعدادات",
"security_tab": "الأمان",
"set_new_avatar": "اختيار صورة رمزية جديدة",
"set_new_profile_background": "اختيار خلفية جديدة للملف الشخصي",
@ -166,7 +264,121 @@
"values": {
"false": "لا",
"true": "نعم"
}
},
"emoji_reactions_scale": "معامل تحجيم التفاعلات",
"app_name": "اسم تطبيق",
"security": "الأمن",
"enter_current_password_to_confirm": "أدخل كلمة السر الحالية لتيقن من هويتك",
"mfa": {
"title": "الاستيثاق بعاملين",
"generate_new_recovery_codes": "ولّد رموز استعادة جديدة",
"warning_of_generate_new_codes": "عند توليد رموز استعادة جديدة ستزال القديمة.",
"recovery_codes": "رموز الاستعادة.",
"recovery_codes_warning": "خزن هذه الرموز في مكان آمن. إذا فقدت هذه الرموز وتعذر عليك الوصول إلى تطبيق الاستيثاق بعاملين، لن تتمكن من الوصول لحسابك.",
"authentication_methods": "طرق الاستيثاق",
"scan": {
"title": "مسح",
"desc": "امسح رمز الاستجابة السريعة QR من تطبيق الاستيثاق أو أدخل المفتاح:",
"secret_code": "مفتاح"
},
"verify": {
"desc": "لتفعيل الاستيثاق بعاملين أدخل الرمز من تطبيق الاستيثاق:"
}
},
"block_import": "استيراد المحجوبين",
"import_mutes_from_a_csv_file": "استورد قائمة المكتومين من ملف csv",
"account_backup": "نسخ احتياطي للحساب",
"download_backup": "نزّل",
"account_backup_table_head": "نسخ احتياطي",
"backup_not_ready": "هذا النسخ الاحتياطي ليس جاهزًا.",
"backup_failed": "فشل النسخ الاحتياطي.",
"remove_backup": "أزل",
"list_backups_error": "خطأ أثناء حلب قائمة النُسخ الاحتياطية: {error}",
"added_backup": "أُضيفت نسخة احتياطية جديدة.",
"blocks_tab": "المحجوبون",
"confirm_dialogs_block": "حجب مستخدم",
"confirm_dialogs_mute": "كتم مستخدم",
"confirm_dialogs_delete": "حذف حالة",
"confirm_dialogs_logout": "خروج",
"confirm_dialogs_approve_follow": "قبول متابِع",
"confirm_dialogs_deny_follow": "رفض متابِع",
"list_aliases_error": "خطأ أثناء جلب الكنيات: {error}",
"hide_list_aliases_error_action": "أغلق",
"remove_alias": "أزل هذه الكنية",
"add_alias_error": "حدث خطأ أثناء إضافة الكنية: {error}",
"confirm_dialogs": "أطلب تأكيدًا عند",
"confirm_dialogs_repeat": "مشاركة حالة",
"mutes_and_blocks": "المكتومون والمحجوبون",
"move_account_target": "الحساب المستهدف (مثل {example})",
"wordfilter": "ترشيح الكلمات",
"always_show_post_button": "أظهر الزر العائم لإنشاء منشور جديد دائمًا",
"hide_wallpaper": "اخف خلفية المثيل",
"save": "احفظ التعديلات",
"lists_navigation": "أظهر القوائم في شريط التنقل",
"mute_export_button": "صدّر قائمة المكتومين إلى ملف csv",
"blocks_imported": "اُستورد المحجوبون! معالجة القائمة ستستغرق وقتًا.",
"mute_export": "تصدير المكتومين",
"mute_import": "استيراد المكتومين",
"mute_import_error": "خطأ أثناء استيراد المكتومين",
"change_email_error": "حدثت خلل أثناء تغيير بريدك الإلكتروني.",
"change_email": "غيّر البريد الإلكتروني",
"changed_email": "نجح تغيير البريد الإلكتروني!",
"account_alias_table_head": "الكنية",
"account_alias": "كنيات الحساب",
"move_account": "أنقل الحساب",
"moved_account": "نُقل الحساب.",
"hide_media_previews": "اخف معاينات الوسائط",
"hide_muted_posts": "اخف منشورات المستخدمين المكتومين",
"confirm_dialogs_unfollow": "الغاء متابعة مستخدم",
"confirm_dialogs_remove_follower": "إزالة متابع",
"new_alias_target": "أضف كنية جديدة (مثل {example})",
"added_alias": "أُضيفت الكنية.",
"move_account_error": "خطأ أثناء نقل الحساب: {error}",
"emoji_reactions_on_timeline": "أظهر التفاعلات في الخط الزمني",
"mutes_imported": "اُستورد المكتومون! معالجة القئمة ستستغرق وقتًا.",
"remove_language": "أزل",
"primary_language": "اللغة الرئيسية:",
"expert_mode": "أظهر الإعدادات المتقدمة",
"block_import_error": "خطأ أثناء استيراد قائمة المحجوبين",
"add_backup": "أنشئ نسخة احتياطية جديدة",
"add_backup_error": "خطأ أثناء إضافة نسخ احتياطي جديد: {error}",
"move_account_notes": "إذا أردت نقل حسابك عليك إضافة كنية تشير إلى هنا في الحساب المستهدف.",
"avatar_size_instruction": "أدنى حجم مستحسن للصورة الرمزية هو 150x150 بيكسل.",
"word_filter_and_more": "مرشح الكلمات والمزيد...",
"hide_all_muted_posts": "اخف المنشورات المكتومة",
"max_thumbnails": "أقصى عدد للصور المصغرة لكل منشور (فارغ = غير محدود)",
"block_export_button": "صدّر قائمة المحجوبين إلى ملف csv",
"block_export": "تصدير المحجوبين",
"use_one_click_nsfw": "افتح المرفقات ذات المحتوى الحساس NSFW بنقرة واحدة",
"account_privacy": "خصوصية",
"use_contain_fit": "لا تقتص الصور المصغرة للمرفقات",
"import_blocks_from_a_csv_file": "استورد المحجوبين من ملف csv",
"instance_default_simple": "(افتراضي)",
"interface": "واجهة",
"birthday": {
"label": "تاريخ الميلاد",
"show_birthday": "اظهر تاريخ ميلادي"
},
"profile_fields": {
"add_field": "أضف حقل",
"value": "محتوى"
},
"posts": "منشورات",
"user_profiles": "ملفات تعريفية للمستخدمين",
"notification_visibility_emoji_reactions": "تفاعلات",
"notification_visibility_polls": "انتهاء استطلاعات اشتركت بها",
"file_export_import": {
"restore_settings": "استرجع الإعدادات من ملف",
"backup_restore": "نسخ احتياطي للإعدادات"
},
"mutes_tab": "مكتومون",
"no_mutes": "لا يوجد مكتومون",
"hide_followers_count_description": "لا تظهر عدد المتابِعين",
"show_moderator_badge": "أظهر شارة \"مشرف\" في ملفي التعريفي",
"hide_follows_count_description": "لا تظهر عدد المتابَعين",
"hide_muted_threads": "اخف النقاشات المكتومة",
"no_blocks": "لا يوجد محجوبون",
"show_admin_badge": "أظهر شارة \"مدير\" في ملفي التعريفي"
},
"timeline": {
"collapse": "",
@ -211,11 +423,109 @@
"keyword_policies": "سياسة الكلمات الدلالية"
},
"simple": {
"simple_policies": "سياسات الخادم"
"simple_policies": "سياسات الخادم",
"instance": "مثيل",
"reason": "السبب",
"accept": "قبول",
"reject": "رفض"
},
"federation": "الاتحاد",
"mrf_policies": "تفعيل سياسات إعادة كتابة المنشور",
"mrf_policies_desc": "خاصية إعادة كتابة المناشير تقوم بتعديل تفاعل الاتحاد مع هذا الخادم. السياسات التالية مفعّلة:"
}
},
"announcements": {
"page_header": "إعلانات",
"title": "إعلان",
"mark_as_read_action": "علّمه كمقروء",
"post_form_header": "انشر إعلانًا",
"post_placeholder": "اكتب محتوى الاعلان هنا...",
"post_action": "انشر",
"post_error": "خطأ: {error}",
"close_error": "أغلاق",
"delete_action": "احذف",
"start_time_prompt": "وقت البدأ: ",
"end_time_prompt": "وقت النهاية: ",
"all_day_prompt": "هذا حدث يوم كامل",
"start_time_display": "يبدأ في {time}",
"end_time_display": "ينتهي في {time}",
"edit_action": "حرر",
"submit_edit_action": "أرسل",
"cancel_edit_action": "ألغِ",
"inactive_message": "هذا الاعلان غير نشط",
"published_time_display": "نُشر في {time}"
},
"polls": {
"votes": "أصوات",
"vote": "صوّت",
"type": "نوع الاستطلاع",
"single_choice": "خيار واحد",
"multiple_choices": "متعدد الخيارات",
"expiry": "عمر الاستطلاع",
"expires_in": "ينتهي الاستطلاع في {0}",
"expired": "انتهى الاستطلاع منذ {0}",
"add_poll": "أضف استطلاعًا",
"add_option": "أضف خيارًا",
"option": "خيار"
},
"emoji": {
"stickers": "ملصقات",
"emoji": "إيموجي",
"search_emoji": "ابحث عن إيموجي",
"unicode_groups": {
"animals-and-nature": "حيوانات وطبيعة",
"food-and-drink": "أطعمة ومشروبات",
"symbols": "رموز",
"activities": "نشاطات",
"flags": "أعلام"
},
"add_emoji": "أدخل إيموجي",
"custom": "إيموجي مخصص"
},
"interactions": {
"emoji_reactions": "تفاعلات بالإيموجي",
"reports": "البلاغات",
"follows": "المتابعات الجديدة"
},
"report": {
"state_closed": "مغلق",
"state_resolved": "عولج",
"reported_statuses": "الحالة المبلغة عنها:",
"state_open": "مفتوح",
"notes": "ملاحظة:",
"state": "الحالة:",
"reporter": "المبلِّغ:",
"reported_user": "المُبلغ عنه:"
},
"selectable_list": {
"select_all": "اختر الكل"
},
"image_cropper": {
"save": "احفظ",
"cancel": "ألغ"
},
"importer": {
"submit": "أرسل",
"success": "نجح الاستيراد.",
"error": "حدث خطأ أثناء الاستيراد."
},
"domain_mute_card": {
"mute": "اكتم",
"mute_progress": "يكتم…",
"unmute": "ارفع الكتم",
"unmute_progress": "يرفع الكتم…"
},
"exporter": {
"export": "صدر",
"processing": "يُعالج. سيُطلب منك تنزيل الملف قريباً"
},
"media_modal": {
"previous": "السابق",
"next": "التالي",
"hide": "أغلق عارض الوسائط"
},
"remote_user_resolver": {
"searching_for": "يبحث عن",
"error": "لم يُعثر عليه."
}
}

View file

@ -176,6 +176,7 @@
"bookmarks": "Bookmarks",
"user_search": "User Search",
"search": "Search",
"search_close": "Close search bar",
"who_to_follow": "Who to follow",
"preferences": "Preferences",
"timelines": "Timelines",
@ -270,6 +271,7 @@
"text/markdown": "Markdown",
"text/bbcode": "BBCode"
},
"content_type_selection": "Post format",
"content_warning": "Subject (optional)",
"default": "Just landed in L.A.",
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
@ -287,6 +289,7 @@
"private": "This post will be visible to your followers only",
"unlisted": "This post will not be visible in Public Timeline and The Whole Known Network"
},
"scope_notice_dismiss": "Close this notice",
"scope": {
"direct": "Direct - post to mentioned users only",
"private": "Followers-only - post to followers only",
@ -462,7 +465,9 @@
"domain_mutes": "Domains",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker",
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
"emoji_reactions_scale": "Reactions scale factor",
"export_theme": "Save preset",
"filtering": "Filtering",
"wordfilter": "Wordfilter",
@ -514,6 +519,8 @@
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"mutes_tab": "Mutes",
"play_videos_in_modal": "Play videos in a popup frame",
"url": "URL",
"preview": "Preview",
"file_export_import": {
"backup_restore": "Settings backup",
"backup_settings": "Backup settings to file",
@ -825,6 +832,98 @@
"title": "Version",
"backend_version": "Backend version",
"frontend_version": "Frontend version"
},
"commit_value": "Save",
"commit_value_tooltip": "Value is not saved, press this button to commit your changes",
"reset_value": "Reset",
"reset_value_tooltip": "Reset draft",
"hard_reset_value": "Hard reset",
"hard_reset_value_tooltip": "Remove setting from storage, forcing use of default value"
},
"admin_dash": {
"window_title": "Administration",
"wip_notice": "This admin dashboard is experimental and WIP, {adminFeLink}.",
"old_ui_link": "old admin UI available here",
"reset_all": "Reset all",
"commit_all": "Save all",
"tabs": {
"nodb": "No DB Config",
"instance": "Instance",
"limits": "Limits",
"frontends": "Front-ends"
},
"nodb": {
"heading": "Database config is disabled",
"text": "You need to change backend config files so that {property} is set to {value}, see more in {documentation}.",
"documentation": "documentation",
"text2": "Most configuration options will be unavailable."
},
"captcha": {
"native": "Native",
"kocaptcha": "KoCaptcha"
},
"instance": {
"instance": "Instance information",
"registrations": "User sign-ups",
"captcha_header": "CAPTCHA",
"kocaptcha": "KoCaptcha settings",
"access": "Instance access",
"restrict": {
"header": "Restrict access for anonymous visitors",
"description": "Detailed setting for allowing/disallowing access to certain aspects of API. By default (indeterminate state) it will disallow if instance is not public, ticked checkbox means disallow access even if instance is public, unticked means allow access even if instance is private. Please note that unexpected behavior might happen if some settings are set, i.e. if profile access is disabled posts will show without profile information.",
"timelines": "Timelines access",
"profiles": "User profiles access",
"activities": "Statues/activities access"
}
},
"limits": {
"arbitrary_limits": "Arbitrary limits",
"posts": "Post limits",
"uploads": "Attachments limits",
"users": "User profile limits",
"profile_fields": "Profile fields limits",
"user_uploads": "Profile media limits"
},
"frontend": {
"repository": "Repository link",
"versions": "Available versions",
"build_url": "Build URL",
"reinstall": "Reinstall",
"is_default": "(Default)",
"is_default_custom": "(Default, version: {version})",
"install": "Install",
"install_version": "Install version {version}",
"more_install_options": "More install options",
"more_default_options": "More default setting options",
"set_default": "Set default",
"set_default_version": "Set version {version} as default",
"wip_notice": "Please note that this section is a WIP and lacks certain features as backend implementation of front-end management is incomplete.",
"default_frontend": "Default front-end",
"default_frontend_tip": "Default front-end will be shown to all users. Currently there's no way to for a user to select personal front-end. If you switch away from PleromaFE you'll most likely have to use old and buggy AdminFE to do instance configuration until we replace it.",
"default_frontend_tip2": "WIP: Since Pleroma backend doesn't properly list all installed frontends you'll have to enter name and reference manually. List below provides shortcuts to fill the values.",
"available_frontends": "Available for install"
},
"temp_overrides": {
":pleroma": {
":instance": {
":public": {
"label": "Instance is public",
"description": "Disabling this will make all API accessible only for logged-in users, this will make Public and Federated timelines inaccessible to anonymous visitors."
},
":limit_to_local_content": {
"label": "Limit search to local content",
"description": "Disables global network search for unauthenticated (default), all users or none"
},
":description_limit": {
"label": "Limit",
"description": "Character limit for attachment descriptions"
},
":background_image": {
"label": "Background image",
"description": "Background image (primarily used by PleromaFE)"
}
}
}
}
},
"time": {
@ -874,6 +973,7 @@
"repeat_confirm_accept_button": "Repeat",
"repeat_confirm_cancel_button": "Do not repeat",
"delete": "Delete status",
"delete_error": "Error deleting status: {0}",
"edit": "Edit status",
"edited_at": "(last edited {time})",
"pin": "Pin on profile",
@ -927,7 +1027,8 @@
"show_all_conversation_with_icon": "{icon} {text}",
"show_all_conversation": "Show full conversation ({numStatus} other status) | Show full conversation ({numStatus} other statuses)",
"show_only_conversation_under_this": "Only show replies to this status",
"status_history": "Status history"
"status_history": "Status history",
"reaction_count_label": "{num} person reacted | {num} people reacted"
},
"user_card": {
"approve": "Approve",
@ -1055,7 +1156,8 @@
"reject_follow_request": "Reject follow request",
"bookmark": "Bookmark",
"toggle_expand": "Expand or collapse notification to show post in full",
"toggle_mute": "Expand or collapse notification to reveal muted content"
"toggle_mute": "Expand or collapse notification to reveal muted content",
"autocomplete_available": "{number} result is available. Use up and down keys to navigate through them. | {number} results are available. Use up and down keys to navigate through them."
},
"upload": {
"error": {

View file

@ -55,8 +55,8 @@
"undo": "Malfari",
"yes": "Jes",
"no": "Ne",
"unpin": "Malfiksi eron",
"pin": "Fiksi eron",
"unpin": "Malfiksi",
"pin": "Fiksi",
"scroll_to_top": "Rulumi supren"
},
"image_cropper": {
@ -81,7 +81,11 @@
"recovery_code": "Rehava kodo",
"enter_two_factor_code": "Enigu kodon de duobla aŭtentikigo",
"enter_recovery_code": "Enigu rehavan kodon",
"authentication_code": "Aŭtentikiga kodo"
"authentication_code": "Aŭtentikiga kodo",
"logout_confirm_title": "Konfirmo de adiaŭo",
"logout_confirm": "Ĉu vi certe volas adiaŭi?",
"logout_confirm_accept_button": "Adiaŭi",
"logout_confirm_cancel_button": "Ne adiaŭi"
},
"media_modal": {
"previous": "Antaŭa",
@ -90,7 +94,7 @@
"hide": "Fermi vidilon de vidaŭdaĵoj"
},
"nav": {
"about": "Pri",
"about": "Prio",
"back": "Reen",
"chat": "Loka babilejo",
"friend_requests": "Petoj pri abono",
@ -115,7 +119,9 @@
"edit_finish": "Fini redakton",
"mobile_notifications": "Malfermi sciigojn (estas nelegitaj)",
"mobile_notifications_close": "Fermi sciigojn",
"announcements": "Anoncoj"
"announcements": "Anoncoj",
"search_close": "Fermi serĉujon",
"mobile_sidebar": "(Mal)ŝalti flankan breton por telefonoj"
},
"notifications": {
"broken_favorite": "Nekonata afiŝo, serĉante ĝin…",
@ -169,7 +175,9 @@
"post": "Afiŝo",
"edit_remote_warning": "Aliaj foraj nodoj eble ne subtenas redaktadon, kaj ne povos ricevi pli novan version de via afiŝo.",
"edit_unsupported_warning": "Pleroma ne subtenas redaktadon de mencioj aŭ enketoj.",
"edit_status": "Redakti afiŝon"
"edit_status": "Redakti afiŝon",
"content_type_selection": "Formo de afiŝo",
"scope_notice_dismiss": "Fermi ĉi tiun avizon"
},
"registration": {
"bio": "Priskribo",
@ -189,14 +197,18 @@
"email_required": "ne povas resti malplena",
"password_required": "ne povas resti malplena",
"password_confirmation_required": "ne povas resti malplena",
"password_confirmation_match": "samu la pasvorton"
"password_confirmation_match": "samu la pasvorton",
"birthday_min_age": "ne povas esti post {date}",
"birthday_required": "ne povas resti malplena"
},
"reason_placeholder": "Ĉi-node oni aprobas registriĝojn permane.\nSciigu la administrantojn kial vi volas registriĝi.",
"reason": "Kialo registriĝi",
"register": "Registriĝi",
"bio_optional": "Prio (malnepra)",
"email_optional": "Retpoŝtadreso (malnepra)",
"email_language": "En kiu lingvo vi volus ricevi retleterojn de la servilo?"
"email_language": "En kiu lingvo vi volus ricevi retleterojn de la servilo?",
"birthday": "Naskiĝtago:",
"birthday_optional": "Naskiĝtago (malnepra):"
},
"settings": {
"app_name": "Nomo de aplikaĵo",
@ -666,7 +678,28 @@
"user_popover_avatar_overlay": "Aperigi ŝprucaĵon pri uzanto sur profilbildo",
"show_yous": "Montri la markon «(Vi)»",
"user_popover_avatar_action_zoom": "Zomi la profilbildon",
"third_column_mode": "Kun sufiĉo da spaco, montri trian kolumnon kun"
"third_column_mode": "Kun sufiĉo da spaco, montri trian kolumnon kun",
"birthday": {
"show_birthday": "Montri mian naskiĝtagon",
"label": "Naskiĝtago"
},
"confirm_dialogs_delete": "forigo de afiŝo",
"backup_running": "Ĉi tiu savkopiado progresas, traktis {number} datumon. | Ĉi tiu savkopiado progresas, traktis {number} datumojn.",
"backup_failed": "Ĉi tiu savkopiado malsukcesis.",
"autocomplete_select_first": "Memage elekti unuan kandidaton kiam rezultoj de memaga konjektado disponeblas",
"confirm_dialogs_logout": "adiaŭo",
"user_popover_avatar_action": "Post klako sur profilbildon en ŝprucaĵo",
"remove_language": "Forigi",
"primary_language": "Ĉefa lingvo:",
"confirm_dialogs": "Peti konfirmon je",
"confirm_dialogs_repeat": "ripeto de afiŝo",
"confirm_dialogs_unfollow": "malabono de uzanto",
"confirm_dialogs_block": "blokado de uzanto",
"confirm_dialogs_mute": "silentigo de uzanto",
"confirm_dialogs_approve_follow": "aprobo de abonanto",
"confirm_dialogs_deny_follow": "malaprobo de abonanto",
"confirm_dialogs_remove_follower": "forigo de abonanto",
"tree_fade_ancestors": "Montri responditojn de la nuna afiŝo per teksto malvigla"
},
"timeline": {
"collapse": "Maletendi",
@ -753,7 +786,33 @@
"note_blank": "(Neniu)",
"edit_note_apply": "Apliki",
"edit_note_cancel": "Nuligi",
"edit_note": "Redakti noton"
"edit_note": "Redakti noton",
"block_confirm": "Ĉu vi certe volas bloki uzanton {user}?",
"block_confirm_accept_button": "Bloki",
"remove_follower_confirm": "Ĉu vi certe volas forigi uzanton {user} de viaj abonantoj?",
"approve_confirm_accept_button": "Aprobi",
"approve_confirm_cancel_button": "Ne aprobi",
"approve_confirm": "Ĉu vi certe volas aprobi abonan peton de {user}?",
"block_confirm_title": "Konfirmo de blokado",
"approve_confirm_title": "Konfirmo de aprobo",
"block_confirm_cancel_button": "Ne bloki",
"deny_confirm_accept_button": "Malaprobi",
"deny_confirm_cancel_button": "Ne malaprobi",
"mute_confirm_title": "Silentigi konfirmon",
"deny_confirm_title": "Konfirmo de malaprobo",
"mute_confirm": "Ĉu vi certe volas silentigi uzanton {user}?",
"mute_confirm_accept_button": "Silentigi",
"mute_confirm_cancel_button": "Ne silentigi",
"mute_duration_prompt": "Silentigi ĉi tiun uzanton por (0 signifas senliman silentigon):",
"remove_follower_confirm_accept_button": "Forigi",
"remove_follower_confirm_title": "Konfirmo de forigo de abonanto",
"birthday": "Naskita je {birthday}",
"deny_confirm": "Ĉu vi certe volas malaprobi abonan peton de {user}?",
"unfollow_confirm_cancel_button": "Ne malaboni",
"unfollow_confirm_title": "Konfirmo de malabono",
"unfollow_confirm": "Ĉu vi certe volas malaboni uzanton {user}?",
"unfollow_confirm_accept_button": "Malaboni",
"remove_follower_confirm_cancel_button": "Ne forigi"
},
"user_profile": {
"timeline_title": "Historio de uzanto",
@ -775,7 +834,8 @@
"accept_follow_request": "Akcepti abonpeton",
"add_reaction": "Aldoni reagon",
"toggle_expand": "Etendi aŭ maletendi sciigon por montri plenan afiŝon",
"toggle_mute": "Etendi aŭ maletendi afiŝon por montri silentigitan enhavon"
"toggle_mute": "Etendi aŭ maletendi afiŝon por montri silentigitan enhavon",
"autocomplete_available": "{number} rezulto disponeblas. Uzu la sagajn klavojn supren kaj suben por foliumi ilin. | {number} rezulto disponeblas. Uzu la sagajn klavojn supren kaj suben por foliumi ilin."
},
"upload": {
"error": {
@ -951,7 +1011,14 @@
"show_all_conversation_with_icon": "{icon} {text}",
"show_only_conversation_under_this": "Montri nur respondojn al ĉi tiu afiŝo",
"status_history": "Historio de afiŝo",
"open_gallery": "Malfermi galerion"
"open_gallery": "Malfermi galerion",
"delete_confirm_title": "Konfirmo de forigo",
"delete_confirm_accept_button": "Forigi",
"repeat_confirm": "Ĉu vi certe volas ripeti ĉi tiun afiŝon?",
"repeat_confirm_title": "Konfirmo de ripeto",
"repeat_confirm_accept_button": "Ripeti",
"repeat_confirm_cancel_button": "Ne ripeti",
"delete_confirm_cancel_button": "Ne forigi"
},
"time": {
"years_short": "{0}j",
@ -1119,6 +1186,7 @@
"edit_action": "Redakti",
"submit_edit_action": "Afiŝi",
"cancel_edit_action": "Nuligi",
"inactive_message": "Ĉi tiu anonco estas neaktiva"
"inactive_message": "Ĉi tiu anonco estas neaktiva",
"post_form_header": "Afiŝi anoncon"
}
}

View file

@ -17,7 +17,17 @@
"media_removal": "メディアをのぞく",
"media_removal_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、とりのぞきます:",
"media_nsfw": "メディアをすべてセンシティブにする",
"media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:"
"media_nsfw_desc": "このインスタンスは、これらのインスタンスからおくられてきたメディアを、すべて、センシティブにマークします:",
"reason": "りゆう",
"instance": "インスタンス",
"not_applicable": "なし"
},
"keyword": {
"keyword_policies": "キーワードポリシー",
"reject": "おことわり",
"replace": "おきかえ",
"ftl_removal": "「つながっているすべてのネットワーク」タイムラインからのぞく",
"is_replaced_by": "→"
}
},
"staff": "スタッフ"
@ -36,7 +46,10 @@
"scope_options": "こうかいはんいせんたく",
"text_limit": "もじのかず",
"title": "ゆうこうなきのう",
"who_to_follow": "おすすめユーザー"
"who_to_follow": "おすすめユーザー",
"pleroma_chat_messages": "Pleroma チャット",
"upload_limit": "アップロードできるファイルのおおきさ",
"shout": "Shoutbox"
},
"finder": {
"error_fetching_user": "ユーザーけんさくがエラーになりました",
@ -54,7 +67,34 @@
"disable": "なし",
"enable": "あり",
"confirm": "たしかめる",
"verify": "たしかめる"
"verify": "たしかめる",
"retry": "もういちど、ためしてください",
"loading": "よみこんでいます…",
"undo": "もとにもどす",
"yes": "はい",
"no": "いいえ",
"unpin": "ピンどめするのをやめる",
"scroll_to_top": "いちばんうえにもどる",
"role": {
"moderator": "モデレーター",
"admin": "かんりするひと"
},
"flash_security": "Flash コンテンツはどんなコードでもじっこうできるので、あぶないかもしれません。",
"flash_fail": "Flash コンテンツをよみこむことに、しっぱいしました。コンソールで、くわしいないようを、よむことができます。",
"scope_in_timeline": {
"private": "フォロワーげんてい",
"public": "パブリック",
"unlisted": "アンリステッド",
"direct": "ダイレクト"
},
"pin": "ピンどめする",
"flash_content": "Flash コンテンツを、 Ruffle をつかってひょうじする (うごかないかもしれません)。",
"generic_error_message": "エラーになりました: {0}",
"error_retry": "もういちど、ためしてください",
"never_show_again": "にどとひょうじしない",
"close": "とじる",
"dismiss": "むしする",
"peek": "かくす"
},
"image_cropper": {
"crop_picture": "がぞうをきりぬく",
@ -83,11 +123,17 @@
"heading": {
"totp": "2-ファクターにんしょう",
"recovery": "2-ファクターリカバリー"
}
},
"logout_confirm_title": "ログアウトのかくにん",
"logout_confirm": "ほんとうに、ログアウトしますか?",
"logout_confirm_accept_button": "ログアウトする",
"logout_confirm_cancel_button": "ログアウトしない"
},
"media_modal": {
"previous": "まえ",
"next": "つぎ"
"next": "つぎ",
"counter": "{current} / {total}",
"hide": "メディアビューアーをとじる"
},
"nav": {
"about": "これはなに?",
@ -104,7 +150,20 @@
"user_search": "ユーザーをさがす",
"search": "さがす",
"who_to_follow": "おすすめユーザー",
"preferences": "せってい"
"preferences": "せってい",
"home_timeline": "ホームタイムライン",
"bookmarks": "ブックマーク",
"timelines": "タイムライン",
"chats": "チャット",
"lists": "リスト",
"mobile_notifications": "つうちをひらく (よんでないものがあります)",
"mobile_notifications_close": "つうちをとじる",
"announcements": "おしらせ",
"edit_pinned": "ピンどめをへんしゅう",
"search_close": "けんさくバーをとじる",
"edit_nav_mobile": "ナビゲーションバーのせっていをかえる",
"mobile_sidebar": "モバイルのサイドバーをきりかえる",
"edit_finish": "へんしゅうをおわりにする"
},
"notifications": {
"broken_favorite": "ステータスがみつかりません。さがしています…",
@ -114,21 +173,29 @@
"notifications": "つうち",
"read": "よんだ!",
"repeated_you": "あなたのステータスがリピートされました",
"no_more_notifications": "つうちはありません"
"no_more_notifications": "つうちはありません",
"error": "つうちをとりにいくことに、しっぱいしました: {0}",
"follow_request": "あなたをフォローしたいです",
"migrated_to": "インスタンスを、ひっこしました",
"reacted_with": "{0} でリアクションしました",
"poll_ended": "とうひょうが、おわりました",
"submitted_report": "つうほうしました"
},
"polls": {
"add_poll": "いれふだをはじめる",
"add_poll": "とうひょうをはじめる",
"add_option": "オプションをふやす",
"option": "オプション",
"votes": "いれふだ",
"vote": "ふだをいれる",
"type": "いれふだのかた",
"votes": "ひょう",
"vote": "とうひょうする",
"type": "とうひょうのけいしき",
"single_choice": "ひとつえらぶ",
"multiple_choices": "いくつでもえらべる",
"expiry": "いれふだのながさ",
"expires_in": "いれふだは {0} で、おわります",
"expired": "いれふだは {0} まえに、おわりました",
"not_enough_options": "ユニークなオプションが、たりません"
"expiry": "とうひょうのながさ",
"expires_in": "とうひょうは {0} で、おわります",
"expired": "とうひょうは {0} まえに、おわりました",
"not_enough_options": "ユニークなオプションが、たりません",
"people_voted_count": "{count} にんが、とうひょうしました",
"votes_count": "{count} ひょう"
},
"emoji": {
"stickers": "ステッカー",
@ -139,7 +206,19 @@
"custom": "カスタムえもじ",
"unicode": "ユニコードえもじ",
"load_all_hint": "はじめの {saneAmount} このえもじだけがロードされています。すべてのえもじをロードすると、パフォーマンスがわるくなるかもしれません。",
"load_all": "すべてのえもじをロード ({emojiAmount} こあります)"
"load_all": "すべてのえもじをロード ({emojiAmount} こあります)",
"unicode_groups": {
"flags": "はた",
"activities": "かつどう",
"animals-and-nature": "どうぶつ・しぜん",
"food-and-drink": "たべもの・のみもの",
"objects": "もの",
"people-and-body": "ひと・からだ",
"smileys-and-emotion": "えがお・きもち",
"symbols": "きごう",
"travel-and-places": "りょこう・ばしょ"
},
"regional_indicator": "ばしょをしめすきごう {letter}"
},
"stickers": {
"add_sticker": "ステッカーをふやす"
@ -147,7 +226,10 @@
"interactions": {
"favs_repeats": "リピートとおきにいり",
"follows": "あたらしいフォロー",
"load_older": "ふるいやりとりをみる"
"load_older": "ふるいやりとりをみる",
"emoji_reactions": "えもじリアクション",
"moves": "ユーザーのひっこし",
"reports": "つうほう"
},
"post_status": {
"new_status": "とうこうする",
@ -176,7 +258,18 @@
"private": "フォロワーげんてい: フォロワーのみにとどきます",
"public": "パブリック: パブリックタイムラインにとどきます",
"unlisted": "アンリステッド: パブリックタイムラインにとどきません"
}
},
"media_description_error": "メディアのアップロードにしっぱいしました。もういちどためしてください",
"edit_status": "ステータスをへんしゅうする",
"media_description": "メディアのせつめい",
"content_type_selection": "とうこうのけいしき",
"edit_remote_warning": "ほかのリモートインスタンスは、へんしゅうをサポートしていないかもしれません。そして、へんしゅうされたとうこうをうけとることができないかもしれません。",
"post": "とうこう",
"edit_unsupported_warning": "Pleroma は、メンションやとうひょうのへんしゅうを、サポートしていません。",
"preview": "プレビュー",
"preview_empty": "なにもありません",
"empty_status_error": "とうこうないようを、にゅうりょくしてください",
"scope_notice_dismiss": "このつうちをとじる"
},
"registration": {
"bio": "プロフィール",
@ -196,8 +289,18 @@
"email_required": "なにかかいてください",
"password_required": "なにかかいてください",
"password_confirmation_required": "なにかかいてください",
"password_confirmation_match": "パスワードがちがいます"
}
"password_confirmation_match": "パスワードがちがいます",
"birthday_required": "なにかかいてください",
"birthday_min_age": "{date} か、それよりまえにしてください"
},
"reason_placeholder": "このインスタンスでは、ひとがかくにんして、とうろくをうけいれています。\nなぜあなたがとうろくしたいのかを、かんりしているひとに、おしえてください。",
"bio_optional": "プロフィール (かかなくてもよい)",
"reason": "とうろくするりゆう",
"email_optional": "Eメール (かかなくてもよい)",
"register": "とうろくする",
"email_language": "サーバーからのメールは、なにご(どのことば)がいいですか?",
"birthday": "たんじょうび:",
"birthday_optional": "たんじょうび (かかなくてもよい):"
},
"remote_user_resolver": {
"remote_user_resolver": "リモートユーザーリゾルバー",
@ -393,7 +496,24 @@
"save_load_hint": "「のこす」オプションをONにすると、テーマをえらんだときとロードしたとき、いまのせっていをのこします。また、テーマをエクスポートするとき、これらのオプションをストアします。すべてのチェックボックスをOFFにすると、テーマをエクスポートしたとき、すべてのせっていをセーブします。",
"reset": "リセット",
"clear_all": "すべてクリア",
"clear_opacity": "とうめいどをクリア"
"clear_opacity": "とうめいどをクリア",
"help": {
"older_version_imported": "ふるいバージョンのフロントエンドでつくられたファイルをインポートしました。",
"snapshot_missing": "ファイルにはテーマのスナップショットがありません。おもっていたみためと、ちがうかもしれません。",
"migration_snapshot_ok": "あんぜんのため、テーマのスナップショットがよみこまれました。テーマのデータをよみこむことができます。",
"snapshot_source_mismatch": "バージョンがただしくないです。フロントエンドのバージョンをもとにもどしたあと、あたらしくしたことが、りゆうかもしれません。ふるいフロントエンドでテーマをへんこうしていたばあい、ふるいバージョンをつかうのがいいです。そうでないばあい、あたらしいバージョンをつかってください。",
"snapshot_present": "テーマのスナップショットをよみこみました。せっていはうわがきされました。かわりに、テーマのじっさいのデータをよみこむことができます。",
"fe_upgraded": "フロントエンドといっしょに、テーマエンジンもあたらしくなりました。",
"fe_downgraded": "フロントエンドが、まえのバージョンにもどりました。",
"migration_napshot_gone": "スナップショットがありません。おぼえているみためと、ちがうかもしれません。",
"upgraded_from_v2": "PleromaFEがあたらしくなったので、いままでのみためとすこしちがうかもしれません。",
"v2_imported": "ふるいフロントエンドのためのファイルをインポートしました。せっていしたのとは、すこしちがうかもしれません。",
"future_version_imported": "あたらしいフロントエンドでつくられたファイルをインポートしました。"
},
"load_theme": "テーマをよみこむ",
"keep_as_is": "そのままにする",
"use_snapshot": "ふるいバージョン",
"use_source": "あたらしいバージョン"
},
"common": {
"color": "いろ",
@ -429,7 +549,26 @@
"borders": "さかいめ",
"buttons": "ボタン",
"inputs": "インプットフィールド",
"faint_text": "うすいテキスト"
"faint_text": "うすいテキスト",
"post": "とうこう / プロフィール",
"wallpaper": "かべがみ",
"icons": "アイコン",
"highlight": "よくみえるようにした、ようそ",
"pressed": "おしたとき",
"chat": {
"border": "さかいめ",
"incoming": "うけとったもの",
"outgoing": "おくったもの"
},
"underlay": "アンダーレイ",
"alert_neutral": "それいがい",
"popover": "ツールチップ、メニュー、ポップオーバー",
"poll": "とうひょうのグラフ",
"selectedPost": "えらんだとうこう",
"selectedMenu": "えらんだメニューアイテム",
"disabled": "つかえないとき",
"toggled": "きりかえたとき",
"tabs": "タブ"
},
"radii": {
"_tab_label": "まるさ"
@ -462,7 +601,8 @@
"buttonPressed": "ボタン (おされているとき)",
"buttonPressedHover": "ボタン (ホバー、かつ、おされているとき)",
"input": "インプットフィールド"
}
},
"hintV3": "かげのばあいは、 {0} というかきかたをつかうことができます。そうすると、ほかのいろのスロットをつかうことができます。"
},
"fonts": {
"_tab_label": "フォント",
@ -497,7 +637,167 @@
"title": "バージョン",
"backend_version": "バックエンドのバージョン",
"frontend_version": "フロントエンドのバージョン"
}
},
"notification_visibility_polls": "あなたがさんかしたとうひょうが、おわりました",
"setting_server_side": "このせっていは、あなたのプロフィールについてのものです。へんこうすると、すべてのセッションとクライアントにえいきょうします",
"mute_import_error": "ミュートのインポートが、エラーになりました",
"account_backup_description": "あなたのアカウントじょうほうや、とうこうのアーカイブを、ダウンロードすることができます。しかし、 Pleroma アカウントにインポートすることはまだできません。",
"list_backups_error": "バックアップリストをとりにいくことが、エラーになりました: {error}",
"list_aliases_error": "エイリアスをとりにいくときに、エラーになりました: {error}",
"added_alias": "エイリアスをつくりました。",
"move_account_notes": "もしあなたがアカウントをほかのインスタンスにひっこしたいのなら、ひっこすさきのアカウントからここへのエイリアスをつくってください。",
"file_export_import": {
"backup_settings_theme": "せっていとテーマをファイルにバックアップする",
"restore_settings": "ファイルからせっていをもとにもどす",
"errors": {
"file_too_new": "メジャーバージョン({fileMajor})がちがいます。この PleromaFE (せっていのバージョン {feMajor}) はふるいので、つかうことができません",
"file_slightly_new": "ファイルのマイナーバージョンがちがっています。いくつかのせっていは、よみこまれないかもしれません",
"invalid_file": "これは Pleroma のせっていをバックアップしたファイルではありません。",
"file_too_old": "メジャーバージョン({fileMajor})がちがいます。ファイルのバージョンが古いので、使うことができません(バージョン {feMajor} いじょうのせっていバージョンをつかってください)"
},
"backup_settings": "せっていをファイルにバックアップする",
"backup_restore": "せっていのバックアップ"
},
"hide_wallpaper": "このインスタンスのバックグラウンドをかくす",
"reply_visibility_following_short": "わたしのフォローしているひとにあてられたリプライをみる",
"reply_visibility_self_short": "じぶんにあてられたリプライだけをみる",
"save": "へんこうをほぞんする",
"reset_banner_confirm": "ほんとうに、バナーをリセットしますか?",
"tree_advanced": "ツリービューで、ナビゲーションをもっとじゅうなんにする",
"third_column_mode": "じゅうぶんなくうかんがあれば、3ばんめのれつをひょうじする",
"conversation_other_replies_button": "「ほかのリプライ」ボタンをひょうじするばしょ",
"user_popover_avatar_action_open": "プロフィールをひらく",
"notification_setting_filters": "フィルター",
"notification_setting_hide_notification_contents": "おくったひとと、ないようを、プッシュつうちにひょうじしない",
"backup_running": "バックアップしています。{number}このデータをしょりしました。",
"word_filter_and_more": "ことばのフィルターと、そのほか…",
"account_privacy": "プライバシー",
"posts": "とうこう",
"move_account": "アカウントをひっこす",
"move_account_target": "ひっこしさきのアカウント (れい: {example})",
"mute_bot_posts": "Bot のとうこうをミュートする",
"hide_bot_indication": "Bot によるとうこうであることを、とうこうにひょうじしない",
"hide_all_muted_posts": "ミュートしたとうこうをかくす",
"hide_shoutbox": "Shoutbox をかくす",
"conversation_display_tree": "ツリーけいしき",
"mention_link_display_full_for_remote": "リモートユーザーだけ、ながいなまえでひょうじする (れい: {'@'}hoge{'@'}example.org)",
"mention_link_bolden_you": "あなたがメンションされたとき、あなたへのメンションを、よくみえるようにする",
"user_popover_avatar_action": "ポップオーバーのアバターをクリックしたとき",
"user_popover_avatar_action_zoom": "アバターをおおきくする",
"user_popover_avatar_action_close": "ポップオーバーをとじる",
"always_show_post_button": "とうこうボタンをいつもひょうじする",
"auto_update": "あたらしいとうこうを、じどうてきにみせる",
"user_mutes": "ユーザー",
"useStreamingApi": "とうこうとつうちを、リアルタイムにうけとる",
"use_websockets": "Websockets をつかう (リアルタイムアップデート)",
"mutes_and_blocks": "ミュートとブロック",
"emoji_reactions_on_timeline": "えもじリアクションをタイムラインにひょうじする",
"accent": "アクセント",
"domain_mutes": "ドメイン",
"import_mutes_from_a_csv_file": "CSVファイルからミュートをインポートする",
"reset_avatar": "アバターをリセットする",
"remove_language": "とりのぞく",
"primary_language": "いちばんわかることば:",
"add_language": "よびとしてつかうことばを、ついかする",
"fallback_language": "よびとしてつかうことば {index}:",
"lists_navigation": "ナビゲーションにリストをひょうじする",
"account_alias": "アカウントのエイリアス",
"mention_link_display_full": "いつも、ながいなまえをひょうじする (れい: {'@'}hoge{'@'}example.org)",
"setting_changed": "せっていは、デフォルトとちがっています",
"email_language": "サーバーからうけとるEメールのことば",
"mute_export": "ミュートのエクスポート",
"mute_export_button": "あなたのミュートを、 CSV ファイルにエクスポートします",
"mute_import": "ミュートのインポート",
"mutes_imported": "ミュートをインポートしました!すこしじかんがかかるかもしれません。",
"account_backup": "アカウントのバックアップ",
"account_backup_table_head": "バックアップ",
"download_backup": "ダウンロード",
"backup_not_ready": "バックアップのじゅんびが、まだできていません。",
"backup_failed": "バックアップにしっぱいしました。",
"remove_backup": "とりのぞく",
"add_backup": "あたらしいバックアップをつくる",
"added_backup": "あたらしいバックアップをつくりました。",
"add_backup_error": "あたらしいバックアップをつくるときに、エラーになりました: {error}",
"bot": "これは bot アカウントです",
"account_alias_table_head": "エイリアス",
"hide_list_aliases_error_action": "とじる",
"remove_alias": "このエイリアスをけす",
"add_alias_error": "エイリアスをつくるときに、エラーになりました: {error}",
"new_alias_target": "あたらしいエイリアスをつくる (れい: {example})",
"moved_account": "アカウントをひっこしました。",
"move_account_error": "アカウントをひっこしているときに、エラーになりました: {error}",
"wordfilter": "ことばのフィルター",
"hide_media_previews": "メディアのプレビューをかくす",
"right_sidebar": "サイドバーをみぎにひょうじする",
"hide_wordfiltered_statuses": "ことばのフィルターでフィルターされたステータスをかくす",
"hide_muted_threads": "ミュートされたスレッドをかくす",
"navbar_column_stretch": "ナビゲーションバーをれつのはばまでのばす",
"birthday": {
"label": "たんじょうび",
"show_birthday": "たんじょうびを、ひょうじする"
},
"profile_fields": {
"label": "プロフィールのメタデータ",
"add_field": "フィールドをふやす",
"name": "ラベル",
"value": "ないよう"
},
"user_profiles": "ユーザープロフィール",
"notification_visibility_moves": "ユーザーのひっこし",
"notification_visibility_emoji_reactions": "リアクション",
"hide_favorites_description": "おきにいりのリストをみせない (つうちはおくられます)",
"reset_profile_background": "プロフィールバックグラウンドをリセットする",
"reset_profile_banner": "プロフィールバナーをリセットする",
"reset_avatar_confirm": "ほんとうに、アバターをリセットしますか?",
"reset_background_confirm": "ほんとうに、バックグラウンドをリセットしますか?",
"column_sizes_sidebar": "サイドバー",
"column_sizes_notifs": "つうち",
"columns": "れつ",
"column_sizes": "れつのおおきさ",
"column_sizes_content": "コンテンツ",
"conversation_display": "スレッドのひょうじけいしき",
"conversation_display_linear": "リニアけいしき",
"conversation_display_linear_quick": "リニアビュー",
"show_scrollbars": "よこのれつにスクロールバーをひょうじする",
"third_column_mode_none": "3ばんめのれつをひょうじしない",
"third_column_mode_postform": "とうこうフォームとナビゲーション",
"third_column_mode_notifications": "つうちのれつをひょうじする",
"tree_fade_ancestors": "げんざいのステータスのおやを、うすいいろのもじでひょうじする",
"conversation_other_replies_button_below": "ステータスのした",
"conversation_other_replies_button_inside": "ステータスのなか",
"max_depth_in_thread": "デフォルトでひょうじするスレッドのふかさ",
"sensitive_by_default": "デフォルトで、とうこうをNSFWにする",
"type_domains_to_mute": "ミュートしたいドメインを、ここでけんさくできます",
"mention_link_use_tooltip": "メンションのリンクをクリックしたとき、ユーザーカードをみせる",
"mention_link_show_avatar": "ユーザーのアバターをリンクのよこにひょうじする",
"mention_link_show_avatar_quick": "ユーザーのアバターをメンションのとなりにひょうじする",
"mention_link_fade_domain": "ドメイン(れい: {'@'}hoge{'@'}example.org のなかの {'@'}example.org)を、うすいいろにする",
"user_popover_avatar_overlay": "ユーザーのポップオーバーを、ユーザーのアバターのうえにひょうじする",
"show_yous": "(あなた)をひょうじする",
"notification_setting_block_from_strangers": "フォローしていないユーザーからのつうちをブロックする",
"notification_setting_privacy": "プライバシー",
"more_settings": "そのたのせってい",
"expert_mode": "くわしいせっていを、ひょうじする",
"mention_links": "メンションのリンク",
"post_look_feel": "とうこうのみためとかんかく",
"allow_following_move": "フォローしているアカウントがインスタンスをひっこしたばあい、じどうでフォローしてもよい",
"chatMessageRadius": "チャットメッセージ",
"confirm_dialogs": "つぎのばあいに、かくにんをする",
"confirm_dialogs_repeat": "ステータスをリピートするとき",
"confirm_dialogs_unfollow": "ユーザーのフォローをはずすとき",
"confirm_dialogs_block": "ユーザーをブロックするとき",
"confirm_dialogs_mute": "ユーザーをミュートするとき",
"confirm_dialogs_delete": "ステータスをけすとき",
"confirm_dialogs_logout": "ログアウトするとき",
"confirm_dialogs_approve_follow": "フォローをうけいれるとき",
"confirm_dialogs_deny_follow": "フォローをことわるとき",
"confirm_dialogs_remove_follower": "フォロワーをとりのぞくとき",
"conversation_display_tree_quick": "ツリービュー",
"disable_sticky_headers": "れつのヘッダーを、がめんのいちばんうえにこていしない",
"virtual_scrolling": "タイムラインのレンダリングをよくする",
"use_at_icon": "{'@'} きごうを、もじのかわりに、アイコンでひょうじする",
"mention_link_display_short": "いつも、みじかいなまえにする (れい: {'@'}hoge)",
"mention_link_display": "メンションのリンクをひょうじするけいしき"
},
"time": {
"day": "{0}日",
@ -531,7 +831,23 @@
"year": "{0}年",
"years": "{0}年",
"year_short": "{0}年",
"years_short": "{0}年"
"years_short": "{0}年",
"unit": {
"minutes": "{0}ふん",
"seconds_short": "{0}びょう",
"weeks": "{0}しゅうかん",
"weeks_short": "{0}しゅう",
"years": "{0}ねん",
"years_short": "{0}ねん",
"days": "{0}にち",
"days_short": "{0}にち",
"hours": "{0}じかん",
"hours_short": "{0}じかん",
"minutes_short": "{0}ふん",
"months": "{0}かげつ",
"months_short": "{0}かげつ",
"seconds": "{0}びょう"
}
},
"timeline": {
"collapse": "たたむ",
@ -543,7 +859,11 @@
"show_new": "よみこみ",
"up_to_date": "さいしん",
"no_more_statuses": "これでおわりです",
"no_statuses": "ありません"
"no_statuses": "ありません",
"socket_broke": "コード{0}により、リアルタイムでつながることがなくなりました",
"socket_reconnected": "リアルタイムでつながることを、つくりました",
"reload": "もういちど、よみこむ",
"error": "タイムラインをとりにいくときに、エラーになりました: {0}"
},
"status": {
"favorites": "おきにいり",
@ -556,7 +876,57 @@
"reply_to": "へんしん:",
"replies_list": "へんしん:",
"mute_conversation": "スレッドをミュートする",
"unmute_conversation": "スレッドをミュートするのをやめる"
"unmute_conversation": "スレッドをミュートするのをやめる",
"repeat_confirm_title": "リピートのかくにん",
"mentions": "メンション",
"thread_muted": "ミュートされたスレッド",
"collapse_attachments": "ファイルをかくす",
"remove_attachment": "ファイルをとりのぞく",
"thread_show_full": "このスレッドのすべてのとうこうをみる (ぜんぶで{numStatus}このステータス、ふかさ{depth})",
"show_all_attachments": "すべてのファイルをみる",
"hide_full_subject": "かくす",
"nsfw": "NSFW",
"hide_content": "かくす",
"status_deleted": "このとうこうは、けされました",
"you": "(あなた)",
"expand": "ひろげる",
"repeat_confirm_accept_button": "リピートする",
"repeat_confirm_cancel_button": "リピートしない",
"edited_at": "({time} まえにへんしゅう)",
"delete_confirm_title": "けすことのかくにん",
"delete_confirm_accept_button": "けす",
"delete_confirm_cancel_button": "のこす",
"edit": "ステータスをへんしゅうする",
"bookmark": "ブックマークする",
"unbookmark": "ブックマークをはずす",
"replies_list_with_others": "へんしん (ほかに +{numReplies}こ):",
"status_unavailable": "ステータスがありません",
"copy_link": "リンクをコピー",
"external_source": "そとにあるソース",
"thread_muted_and_words": "つぎのことばをふくむので:",
"show_content": "みる",
"plus_more": "あと {number}こ",
"many_attachments": "とうこうには、{number}このファイルがついています",
"show_attachment_in_modal": "メディアモーダルでみる",
"show_attachment_description": "せつめいのプレビュー (ぜんぶみるには、ファイルをひらいてください)",
"hide_attachment": "ファイルをかくす",
"attachment_stop_flash": "Flash プレーヤーをとめる",
"move_up": "ファイルをひだりにうごかす",
"move_down": "ファイルをみぎにうごかす",
"open_gallery": "ギャラリーをひらく",
"thread_hide": "スレッドをかくす",
"thread_show": "スレッドをみる",
"show_full_subject": "すべてをみる",
"repeat_confirm": "ほんとうに、このステータスをリピートしますか?",
"show_all_conversation": "このスレッドをぜんぶみる (あと {numStatus}このステータス)",
"show_only_conversation_under_this": "このステータスへのへんしんだけをみる",
"status_history": "ステータスのれきし",
"thread_show_full_with_icon": "{icon} {text}",
"thread_follow": "のこりのとうこうをみる (ぜんぶで {numStatus}このステータス)",
"thread_follow_with_icon": "{icon} {text}",
"ancestor_follow": "このステータスよりしたの、{numReplies}このへんしんをみる",
"ancestor_follow_with_icon": "{icon} {text}",
"show_all_conversation_with_icon": "{icon} {text}"
},
"user_card": {
"approve": "うけいれ",
@ -577,7 +947,7 @@
"media": "メディア",
"mention": "メンション",
"mute": "ミュート",
"muted": "ミュートしています",
"muted": "ミュートしています",
"per_day": "/日",
"remote_follow": "リモートフォロー",
"report": "つうほう",
@ -608,8 +978,52 @@
"disable_remote_subscription": "ほかのインスタンスからフォローされないようにする",
"disable_any_subscription": "フォローされないようにする",
"quarantine": "ほかのインスタンスのユーザーのとうこうをとめる",
"delete_user": "ユーザーをけす"
}
"delete_user": "ユーザーをけす",
"delete_user_data_and_deactivate_confirmation": "これをすると、このアカウントのデータがきえて、にどとつかえなくなります。ほんとうに、していいですか?"
},
"mute_confirm_accept_button": "ミュートする",
"unfollow_confirm_title": "フォローをやめることのかくにん",
"mute_confirm": "ほんとうに、 {user} をミュートしますか?",
"mute_duration_prompt": "このユーザーをつぎのじかんだけミュートする (0にすると、おわりがありません):",
"edit_note_apply": "てきよう",
"block_confirm": "ほんとうに、 {user} をブロックしますか?",
"deactivated": "つかえない",
"remove_follower": "フォロワーをとりのぞく",
"highlight": {
"solid": "バッググラウンドをひとつのいろにする",
"striped": "しまもようのバックグラウンドにする",
"side": "はじにせんをつける",
"disabled": "めだたせない"
},
"mute_confirm_cancel_button": "ミュートしない",
"unfollow_confirm_accept_button": "フォローをやめる",
"unfollow_confirm": "ほんとうに、 {user} のフォローをやめますか?",
"unfollow_confirm_cancel_button": "フォローしたままにする",
"mute_confirm_title": "ミュートのかくにん",
"block_confirm_accept_button": "ブロックする",
"block_confirm_cancel_button": "ブロックしない",
"deny_confirm_title": "おことわりのかくにん",
"deny_confirm_accept_button": "ことわる",
"deny_confirm_cancel_button": "ことわらない",
"deny_confirm": "{user} のフォローリクエストをことわりますか?",
"follow_cancel": "リクエストをキャンセル",
"birthday": "{birthday} に、うまれました",
"remove_follower_confirm_title": "フォロワーをとりのぞくことのかくにん",
"remove_follower_confirm_accept_button": "とりのぞく",
"remove_follower_confirm_cancel_button": "のこす",
"remove_follower_confirm": "ほんとうに、 {user} をあなたのフォロワーからとりのぞきますか?",
"edit_note": "メモをへんしゅうする",
"edit_note_cancel": "キャンセル",
"message": "メッセージ",
"bot": "bot",
"approve_confirm_title": "うけいれのかくにん",
"approve_confirm_accept_button": "うけいれる",
"approve_confirm_cancel_button": "うけいれない",
"approve_confirm": "{user} のフォローリクエストをうけいれますか?",
"edit_profile": "プロフィールをへんしゅう",
"block_confirm_title": "ブロックのかくにん",
"note_blank": "(なし)",
"note": "メモ"
},
"user_profile": {
"timeline_title": "ユーザータイムライン",
@ -634,13 +1048,21 @@
"repeat": "リピート",
"reply": "リプライ",
"favorite": "おきにいり",
"user_settings": "ユーザーせってい"
"user_settings": "ユーザーせってい",
"accept_follow_request": "フォローのおねがいを、うけいれる",
"toggle_mute": "ミュートされたないようをみるために、つうちをひらくか、とじる",
"autocomplete_available": "{number}このけっかが、あります。うえとしたのキーをつかって、けっかをみることができます。",
"add_reaction": "リアクションをつける",
"reject_follow_request": "フォローのおねがいを、ことわる",
"bookmark": "ブックマーク",
"toggle_expand": "とうこうをすべてみるために、つうちをひらくか、とじる"
},
"upload": {
"error": {
"base": "アップロードにしっぱいしました。",
"file_too_big": "ファイルがおおきすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]",
"default": "しばらくしてから、ためしてください"
"default": "しばらくしてから、ためしてください",
"message": "アップロードにしっぱいしました: {0}"
},
"file_size_units": {
"B": "B",
@ -655,7 +1077,9 @@
"hashtags": "ハッシュタグ",
"person_talking": "{count} にんが、はなしています",
"people_talking": "{count} にんが、はなしています",
"no_results": "みつかりませんでした"
"no_results": "みつかりませんでした",
"no_more_results": "これでおわりです",
"load_more": "もっとみる"
},
"password_reset": {
"forgot_password": "パスワードを、わすれましたか?",
@ -668,5 +1092,103 @@
"password_reset_disabled": "このインスタンスでは、パスワードリセットは、できません。インスタンスのアドミニストレーターに、おといあわせください。",
"password_reset_required": "ログインするには、パスワードをリセットしてください。",
"password_reset_required_but_mailer_is_disabled": "あなたはパスワードのリセットがひつようです。しかし、まずいことに、このインスタンスでは、パスワードのリセットができなくなっています。このインスタンスのアドミニストレーターに、おといあわせください。"
},
"announcements": {
"post_placeholder": "おしらせのないようを、にゅうりょくしてください。",
"end_time_prompt": "おわるじかん: ",
"inactive_message": "このおしらせは、つかわれていません",
"page_header": "おしらせ",
"title": "おしらせ",
"post_action": "とうこう",
"post_form_header": "おしらせをとうこう",
"mark_as_read_action": "よんだことにする",
"post_error": "エラー: {error}",
"close_error": "とじる",
"delete_action": "けす",
"start_time_display": "{time}にはじまります",
"end_time_display": "{time}におわります",
"edit_action": "へんしゅう",
"start_time_prompt": "はじまるじかん: ",
"all_day_prompt": "このイベントはいちにちじゅうやります",
"published_time_display": "{time}にこうかいされました",
"submit_edit_action": "そうしん",
"cancel_edit_action": "キャンセル"
},
"report": {
"reported_statuses": "つうほうされたステータス:",
"reporter": "つうほうしたひと:",
"state_closed": "クローズ",
"state_resolved": "かいけつしました",
"reported_user": "つうほうされたユーザー:",
"notes": "メモ:",
"state": "じょうたい:",
"state_open": "オープン"
},
"update": {
"update_bugs": "もんだいや、バグがあれば、 {pleromaGitlab} でおしえてください。ちゃんとテストはしているのですが、たくさんのことをかえているので、そしてかいはつバージョンをつかっているので、もんだいやバグに、きづかないことがあります。あなたがきづいたもんだいについての、フィードバックやていあんを、まっています。 Pleroma や Pleroma-FE をよくするやりかたについても、おしえてください。",
"update_changelog_here": "すべてのかわったことのきろく",
"art_by": "{linkToArtist}によるさくひん",
"big_update_title": "すこし、まってください",
"big_update_content": "しばらくリリースがありませんでした。おもっていたみためと、ちがうかもしれません。",
"update_bugs_gitlab": "Pleroma GitLab",
"update_changelog": "かわったことをすべてみるには、{theFullChangelog}をみてください。"
},
"chats": {
"new": "あたらしいチャット",
"chats": "チャット",
"you": "あなた:",
"message_user": "{nickname} にメッセージ",
"delete": "けす",
"empty_message_error": "なにかかいてください",
"more": "もっとみる",
"delete_confirm": "ほんとうに、このメッセージをけしますか?",
"error_loading_chat": "チャットをよみこむことに、しっぱいしました。",
"error_sending_message": "メッセージをおくることに、しっぱいしました。",
"empty_chat_list_placeholder": "チャットがありません。あたらしいチャットボタンをおして、はじめてください!"
},
"shoutbox": {
"title": "Shoutbox"
},
"errors": {
"storage_unavailable": "Pleroma はブラウザーのストレージにアクセスすることができません。あなたがログインしたことと、あなたのローカルのせっていは、ほぞんされません。ほかにももんだいがおきるかもしれません。 Cookie をゆうこうにしてください。"
},
"lists": {
"lists": "リスト",
"new": "あたらしいリスト",
"search": "ユーザーをさがす",
"title": "リストのなまえ",
"create": "つくる",
"save": "へんこうをほぞんする",
"delete": "リストをけす",
"following_only": "フォローしているひとげんていにする",
"manage_lists": "リストをかんりする",
"manage_members": "リストにふくまれるひとを、かんりする",
"add_members": "もっとユーザーをさがす",
"remove_from_list": "リストからとりのぞく",
"add_to_list": "リストにいれる",
"editing_list": "リスト {listTitle} をへんしゅうしています",
"creating_list": "あたらしいリストをつくっています",
"update_title": "なまえをほぞんする",
"really_delete": "ほんとうに、リストをけしますか?",
"is_in_list": "すでにリストのなかにあります",
"error": "リストをへんしゅうするときに、エラーになりました: {0}"
},
"file_type": {
"audio": "オーディオ",
"video": "ビデオ",
"image": "がぞう",
"file": "ファイル"
},
"display_date": {
"today": "きょう"
},
"unicode_domain_indicator": {
"tooltip": "このドメインは、ASCIIいがいのもじをふくんでいます。"
},
"domain_mute_card": {
"mute": "ミュート",
"mute_progress": "ミュートしています…",
"unmute": "ミュートをやめる",
"unmute_progress": "ミュートをやめています…"
}
}

View file

@ -75,7 +75,11 @@
"enter_two_factor_code": "2단계인증 코드를 입력하십시오",
"enter_recovery_code": "복구 코드를 입력하십시오",
"authentication_code": "인증 코드",
"hint": "로그인해서 대화에 참여"
"hint": "로그인해서 대화에 참여",
"logout_confirm_title": "로그아웃 확인",
"logout_confirm": "정말 로그아웃 하시겠습니까?",
"logout_confirm_accept_button": "로그아웃",
"logout_confirm_cancel_button": "로그아웃 안 함"
},
"nav": {
"about": "인스턴스 소개",
@ -104,7 +108,8 @@
"edit_finish": "편집 종료",
"mobile_notifications_close": "알림 닫기",
"mobile_sidebar": "모바일 사이드바 토글",
"announcements": "공지사항"
"announcements": "공지사항",
"search_close": "검색 바 닫기"
},
"notifications": {
"broken_favorite": "알 수 없는 게시물입니다, 검색합니다…",
@ -158,7 +163,9 @@
"edit_status": "수정",
"edit_remote_warning": "수정 기능이 없는 다른 인스턴스에서는 수정한 사항이 반영되지 않을 수 있습니다.",
"post": "게시",
"direct_warning_to_first_only": "맨 앞에 멘션한 사용자들에게만 보여집니다."
"direct_warning_to_first_only": "맨 앞에 멘션한 사용자들에게만 보여집니다.",
"content_type_selection": "게시물 형태",
"scope_notice_dismiss": "알림 닫기"
},
"registration": {
"bio": "소개",
@ -175,7 +182,9 @@
"email_required": "공백으로 둘 수 없습니다",
"password_required": "공백으로 둘 수 없습니다",
"password_confirmation_required": "공백으로 둘 수 없습니다",
"password_confirmation_match": "패스워드와 일치해야 합니다"
"password_confirmation_match": "패스워드와 일치해야 합니다",
"birthday_required": "공백으로 둘 수 없습니다",
"birthday_min_age": "{date} 또는 그 이전 출생만 가능합니다"
},
"fullname_placeholder": "예: 김례인",
"username_placeholder": "예: lain",
@ -185,7 +194,9 @@
"reason": "가입하려는 이유",
"reason_placeholder": "이 인스턴스는 수동으로 가입을 승인하고 있습니다.\n왜 가입하고 싶은지 관리자에게 알려주세요.",
"register": "가입",
"email_language": "무슨 언어로 이메일을 받길 원하시나요?"
"email_language": "무슨 언어로 이메일을 받길 원하시나요?",
"birthday": "생일:",
"birthday_optional": "생일 (선택):"
},
"settings": {
"attachmentRadius": "첨부물",
@ -383,7 +394,8 @@
"highlight": "강조 요소",
"pressed": "눌렸을 때",
"toggled": "토글됨",
"tabs": "탭"
"tabs": "탭",
"underlay": "밑배경"
},
"radii": {
"_tab_label": "둥글기"
@ -652,7 +664,29 @@
"post_status_content_type": "게시물 내용 형식",
"list_aliases_error": "별칭을 가져오는 중 에러 발생: {error}",
"add_alias_error": "별칭을 추가하는 중 에러 발생: {error}",
"mention_link_show_avatar_quick": "멘션 옆에 유저 프로필 사진을 보임"
"mention_link_show_avatar_quick": "멘션 옆에 유저 프로필 사진을 보임",
"backup_running": "백업 중입니다, {number}개 처리 완료. | 백업 중입니다, {number}개 처리 완료.",
"confirm_dialogs": "하기 전에 다시 물어보기",
"autocomplete_select_first": "자동완성이 가능하면 자동으로 첫 번째 후보를 선택",
"backup_failed": "백업에 실패했습니다.",
"emoji_reactions_scale": "리액션 크기",
"birthday": {
"label": "생일",
"show_birthday": "내 생일 보여주기"
},
"add_language": "보조 언어 추가",
"confirm_dialogs_repeat": "리핏",
"confirm_dialogs_unfollow": "언팔로우",
"confirm_dialogs_block": "차단",
"confirm_dialogs_mute": "뮤트",
"confirm_dialogs_delete": "게시물 삭제",
"confirm_dialogs_approve_follow": "팔로워 승인",
"confirm_dialogs_deny_follow": "팔로워 거절",
"confirm_dialogs_remove_follower": "팔로워 제거",
"remove_language": "삭제",
"primary_language": "주 언어:",
"fallback_language": "보조 언어 {index}:",
"confirm_dialogs_logout": "로그아웃"
},
"timeline": {
"collapse": "접기",
@ -735,7 +769,12 @@
"striped": "줄무늬 배경",
"solid": "단색 배경",
"side": "옆트임"
}
},
"approve_confirm_title": "승인 확인",
"approve_confirm_accept_button": "승인",
"approve_confirm_cancel_button": "승인 안 함",
"approve_confirm": "{user}의 팔로우 요청을 승인할까요?",
"block_confirm_title": "차단 확인"
},
"user_profile": {
"timeline_title": "사용자 타임라인",
@ -1069,7 +1108,14 @@
"ancestor_follow_with_icon": "{icon} {text}",
"show_all_conversation_with_icon": "{icon} {text}",
"ancestor_follow": "이 게시물 아래 {numReplies}개 답글 더 보기 | 이 게시물 아래 {numReplies}개 답글 더 보기",
"show_only_conversation_under_this": "이 게시물의 답글만 보기"
"show_only_conversation_under_this": "이 게시물의 답글만 보기",
"repeat_confirm": "리핏할까요?",
"repeat_confirm_title": "리핏 확인",
"repeat_confirm_accept_button": "리핏",
"repeat_confirm_cancel_button": "리핏 안 함",
"delete_confirm_title": "삭제 확인",
"delete_confirm_accept_button": "삭제",
"delete_confirm_cancel_button": "냅두기"
},
"errors": {
"storage_unavailable": "Pleroma가 브라우저 저장소에 접근할 수 없습니다. 로그인이 풀리거나 로컬 설정이 초기화 되는 등 예상치 못한 문제를 겪을 수 있습니다. 쿠키를 활성화 해보세요."

View file

@ -118,7 +118,10 @@
"totp": "Двофакторна автентифікація"
},
"enter_two_factor_code": "Введіть двофакторний код автентифікації",
"placeholder": "напр. stepan"
"placeholder": "напр. stepan",
"logout_confirm": "Ви дійсно хочете вийти?",
"logout_confirm_accept_button": "Вийти",
"logout_confirm_cancel_button": "Ні, хочу назад!"
},
"importer": {
"error": "Під час імпортування файлу сталася помилка.",
@ -189,7 +192,8 @@
"mobile_notifications": "Відкрити сповіщення (є непрочитані)",
"mobile_notifications_close": "Закрити сповіщення",
"edit_nav_mobile": "Редагувати панель навігації",
"announcements": "Анонси"
"announcements": "Анонси",
"search_close": "Закрити панель пошуку"
},
"media_modal": {
"next": "Наступна",

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