logo

pleroma-fe

My custom branche(s) on git.pleroma.social/pleroma/pleroma-fe git clone https://hacktivis.me/git/pleroma-fe.git
commit: 2e7bd99444546b3a71e1ff0753e12e6706c8228e
parent 9a8bc245a6f76f1a41da9d05408dadc36625ffe9
Author: Henry Jameson <me@hjkos.com>
Date:   Mon,  8 Mar 2021 22:01:28 +0200

Merge remote-tracking branch 'origin/develop' into websocket-fixes

* origin/develop: (119 commits)
  Apply 1 suggestion(s) to 1 file(s)
  Make it possible to localize user highlight options
  remove shoutbox test hacks
  fix shoutbox header, use custom scroll-to-bottom system, remove vue-chat-scroll, temporarily add chat test hack
  update changelog with 2.3.0
  change icons around
  Translated using Weblate (Japanese)
  Update timeline_quick_settings.js
  add screen_name_ui to tests
  separate screen_name and screen_name_ui with decoded punycode
  Update CHANGELOG.md
  add basic validation for statusless status notifications
  changelog mention
  fix chat unread badge
  update shelljs to get rid of warnings on build
  save a few characters
  focus input in emoji picker and react picker
  fix vue warnings
  add only to wording
  basic loggedin check for reply filtering
  ...

Diffstat:

A.mailmap2++
MCHANGELOG.md30+++++++++++++++++++++++++++++-
Mpackage.json3+--
Msrc/App.scss13+++++++++++++
Msrc/boot/after_store.js1+
Msrc/components/basic_user_card/basic_user_card.vue2+-
Msrc/components/chat/chat.js10+++++++++-
Msrc/components/chat/chat.scss6+++---
Msrc/components/chat_message_date/chat_message_date.vue4+++-
Msrc/components/chat_panel/chat_panel.js12++++++++++++
Msrc/components/chat_panel/chat_panel.vue15++++++++++-----
Msrc/components/chat_title/chat_title.js2+-
Msrc/components/conversation/conversation.vue1-
Msrc/components/emoji_input/emoji_input.js8++++++++
Msrc/components/emoji_input/emoji_input.vue1+
Msrc/components/emoji_input/suggestor.js4++--
Msrc/components/extra_buttons/extra_buttons.vue5+++++
Msrc/components/interface_language_switcher/interface_language_switcher.vue28+++++++++++++---------------
Msrc/components/media_modal/media_modal.vue10++++++++++
Msrc/components/mfa_form/recovery_form.vue2++
Msrc/components/mfa_form/totp_form.vue2++
Msrc/components/moderation_tools/moderation_tools.vue33+++++++--------------------------
Msrc/components/notification/notification.vue10+++++-----
Msrc/components/poll/poll.vue7++++++-
Msrc/components/poll/poll_form.vue31++++++++++---------------------
Msrc/components/popover/popover.js14++++++++++++--
Msrc/components/popover/popover.vue36+++++++++++++++++++++++++++++++-----
Msrc/components/post_status_form/post_status_form.js4++--
Msrc/components/post_status_form/post_status_form.vue26++++++--------------------
Msrc/components/react_button/react_button.js6++++++
Msrc/components/react_button/react_button.vue103++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/components/registration/registration.js17+++++++++++++----
Msrc/components/registration/registration.vue17+++++++++++++++++
Msrc/components/scope_selector/scope_selector.vue4++++
Msrc/components/search/search.vue1+
Msrc/components/search_bar/search_bar.vue3+++
Asrc/components/settings_modal/helpers/boolean_setting.vue57+++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/settings_modal/helpers/modified_indicator.vue51+++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/settings_modal/helpers/shared_computed_object.js22++++------------------
Msrc/components/settings_modal/tabs/filtering_tab.js4++--
Msrc/components/settings_modal/tabs/filtering_tab.vue44++++++++++++++++++++++----------------------
Msrc/components/settings_modal/tabs/general_tab.js4++--
Msrc/components/settings_modal/tabs/general_tab.vue131+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/components/settings_modal/tabs/profile_tab.scss9+++++----
Msrc/components/settings_modal/tabs/profile_tab.vue14+++++++-------
Msrc/components/settings_modal/tabs/security_tab/security_tab.js3++-
Msrc/components/side_drawer/side_drawer.vue2+-
Msrc/components/staff_panel/staff_panel.js20+++++++++++++++++---
Msrc/components/staff_panel/staff_panel.vue27++++++++++++++++++++++-----
Msrc/components/status/status.js5+++--
Msrc/components/status/status.vue6+++---
Msrc/components/tab_switcher/tab_switcher.js4+++-
Msrc/components/timeago/timeago.vue6++++--
Msrc/components/timeline/timeline.js9++++++---
Msrc/components/timeline/timeline.vue4++++
Asrc/components/timeline/timeline_quick_settings.js63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/components/timeline/timeline_quick_settings.vue107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/components/user_avatar/user_avatar.vue4++--
Msrc/components/user_card/user_card.vue17++++++++---------
Msrc/components/user_list_popover/user_list_popover.vue2+-
Msrc/components/user_reporting_modal/user_reporting_modal.vue2+-
Msrc/i18n/en.json25++++++++++++++++++++++++-
Msrc/i18n/eo.json27++++++++++++++++++++-------
Msrc/i18n/es.json9+++++++--
Msrc/i18n/fr.json81++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------
Msrc/i18n/it.json52+++++++++++++++++++++++++++++++---------------------
Msrc/i18n/ja_pedantic.json338++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/i18n/ko.json256++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msrc/i18n/nb.json37++++++++++++++++++++-----------------
Msrc/i18n/pt.json591+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/i18n/ru.json57+++++++++++++++++++++++++++++++++++++++++++++------------
Msrc/i18n/uk.json32+++++++++++++++++++-------------
Msrc/i18n/zh.json20+++++++++++++++-----
Msrc/i18n/zh_Hant.json38+++++++++++++++++++++++++-------------
Msrc/main.js2--
Msrc/modules/chat.js1+
Msrc/modules/chats.js6++++++
Msrc/modules/config.js25+++++++++++++++----------
Msrc/modules/instance.js1+
Msrc/modules/statuses.js29++++++++++++++++++++++-------
Msrc/services/chat_service/chat_service.js17+++++++++++++++++
Msrc/services/entity_normalizer/entity_normalizer.service.js12+++++++++---
Asrc/services/locale/locale.service.js12++++++++++++
Msrc/services/notification_utils/notification_utils.js7+++++++
Msrc/services/style_setter/style_setter.js15++++++++++++---
Mtest/unit/specs/components/user_profile.spec.js6++++--
Mtest/unit/specs/services/chat_service/chat_service.spec.js17+++++++++++++++++
Mtest/unit/specs/services/entity_normalizer/entity_normalizer.spec.js2+-
Myarn.lock11++++-------
89 files changed, 2191 insertions(+), 625 deletions(-)

diff --git a/.mailmap b/.mailmap @@ -0,0 +1 @@ +rinpatch <rin@patch.cx> <rinpatch@sdf.org> +\ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md @@ -3,9 +3,33 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - ## [Unreleased] ### Added +- Added a quick settings to timeline header for easier access +- Added option to mark posts as sensitive by default + +## [2.3.0] - 2021-03-01 +### Fixed +- Button to remove uploaded media in post status form is now properly placed and sized. +- Fixed shoutbox not working in mobile layout +- Fixed missing highlighted border in expanded conversations again +- Fixed some UI jumpiness when opening images particularly in chat view +- Fixed chat unread badge looking weird +- Fixed punycode names not working properly +- Fixed notifications crashing on an invalid notification + +### Changed +- Display 'people voted' instead of 'votes' for multi-choice polls +- Optimized chat to not get horrible performance after keeping the same chat open for a long time +- When opening emoji picker or react picker, it automatically focuses the search field +- Language picker now uses native language names + +### Added +- Added reason field for registration when approval is required +- Group staff members by role in the About page + +## [2.2.3] - 2021-01-18 +### Added - Added Report button to status ellipsis menu for easier reporting ### Fixed @@ -16,6 +40,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix not being able to re-enable websocket until page refresh - Fix annoying issue where timeline might have few posts when streaming is enabled +### Changed +- Don't filter own posts when they hit your wordfilter + + ## [2.2.2] - 2020-12-22 ### Added - Mouseover titles for emojis in reaction picker diff --git a/package.json b/package.json @@ -34,7 +34,6 @@ "punycode.js": "^2.1.0", "v-click-outside": "^2.1.1", "vue": "^2.6.11", - "vue-chat-scroll": "^1.2.1", "vue-i18n": "^7.3.2", "vue-router": "^3.0.1", "vue-template-compiler": "^2.6.11", @@ -103,7 +102,7 @@ "selenium-server": "2.53.1", "semver": "^5.3.0", "serviceworker-webpack-plugin": "^1.0.0", - "shelljs": "^0.7.4", + "shelljs": "^0.8.4", "sinon": "^2.1.0", "sinon-chai": "^2.8.0", "stylelint": "^13.6.1", diff --git a/src/App.scss b/src/App.scss @@ -178,6 +178,13 @@ a { &.-fullwidth { width: 100%; } + + &.-hover-highlight { + &:hover svg { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } + } } input, textarea, .select, .input { @@ -579,6 +586,7 @@ nav { color: var(--faint, $fallback--faint); box-shadow: 0px 0px 4px rgba(0,0,0,.6); box-shadow: var(--topBarShadow); + box-sizing: border-box; } .fade-enter-active, .fade-leave-active { @@ -880,6 +888,11 @@ nav { overflow: hidden; height: 100%; + // Get rid of scrollbar on body as scrolling happens on different element + body { + overflow: hidden; + } + // Ensures the fixed position of the mobile browser bars on scroll up / down events. // Prevents the mobile browser bars from overlapping or hiding the message posting form. @media all and (max-width: 800px) { diff --git a/src/boot/after_store.js b/src/boot/after_store.js @@ -51,6 +51,7 @@ const getInstanceConfig = async ({ store }) => { const vapidPublicKey = data.pleroma.vapid_public_key store.dispatch('setInstanceOption', { name: 'textlimit', value: textlimit }) + store.dispatch('setInstanceOption', { name: 'accountApprovalRequired', value: data.approval_required }) if (vapidPublicKey) { store.dispatch('setInstanceOption', { name: 'vapidPublicKey', value: vapidPublicKey }) diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue @@ -42,7 +42,7 @@ class="basic-user-card-screen-name" :to="userProfileLink(user)" > - @{{ user.screen_name }} + @{{ user.screen_name_ui }} </router-link> </div> <slot /> diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js @@ -73,7 +73,7 @@ const Chat = { }, formPlaceholder () { if (this.recipient) { - return this.$t('chats.message_user', { nickname: this.recipient.screen_name }) + return this.$t('chats.message_user', { nickname: this.recipient.screen_name_ui }) } else { return '' } @@ -234,6 +234,13 @@ const Chat = { const scrollable = this.$refs.scrollable return scrollable && scrollable.scrollTop <= 0 }, + cullOlderCheck () { + window.setTimeout(() => { + if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { + this.$store.dispatch('cullOlderMessages', this.currentChatMessageService.chatId) + } + }, 5000) + }, handleScroll: _.throttle(function () { if (!this.currentChat) { return } @@ -241,6 +248,7 @@ const Chat = { this.fetchChat({ maxId: this.currentChatMessageService.minId }) } else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { this.jumpToBottomButtonVisible = false + this.cullOlderCheck() if (this.newMessageCount > 0) { // Use a delay before marking as read to prevent situation where new messages // arrive just as you're leaving the view and messages that you didn't actually diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss @@ -98,10 +98,10 @@ .unread-message-count { font-size: 0.8em; left: 50%; - transform: translate(-50%, 0); - border-radius: 100%; margin-top: -1rem; - padding: 0; + padding: 0.1em; + border-radius: 50px; + position: absolute; } .chat-loading-error { diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue @@ -5,6 +5,8 @@ </template> <script> +import localeService from 'src/services/locale/locale.service.js' + export default { name: 'Timeago', props: ['date'], @@ -16,7 +18,7 @@ export default { if (this.date.getTime() === today.getTime()) { return this.$t('display_date.today') } else { - return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' }) + return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' }) } } } diff --git a/src/components/chat_panel/chat_panel.js b/src/components/chat_panel/chat_panel.js @@ -35,6 +35,18 @@ const chatPanel = { userProfileLink (user) { return generateProfileLink(user.id, user.username, this.$store.state.instance.restrictedNicknames) } + }, + watch: { + messages (newVal) { + const scrollEl = this.$el.querySelector('.chat-window') + if (!scrollEl) return + if (scrollEl.scrollTop + scrollEl.offsetHeight + 20 > scrollEl.scrollHeight) { + this.$nextTick(() => { + if (!scrollEl) return + scrollEl.scrollTop = scrollEl.scrollHeight - scrollEl.offsetHeight + }) + } + } } } diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue @@ -10,17 +10,15 @@ @click.stop.prevent="togglePanel" > <div class="title"> - <span>{{ $t('shoutbox.title') }}</span> + {{ $t('shoutbox.title') }} <FAIcon v-if="floating" icon="times" + class="close-icon" /> </div> </div> - <div - v-chat-scroll - class="chat-window" - > + <div class="chat-window"> <div v-for="message in messages" :key="message.id" @@ -94,6 +92,13 @@ .icon { color: $fallback--text; color: var(--text, $fallback--text); + margin-right: 0.5em; + } + + .title { + display: flex; + justify-content: space-between; + align-items: center; } } diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js @@ -12,7 +12,7 @@ export default Vue.component('chat-title', { ], computed: { title () { - return this.user ? this.user.screen_name : '' + return this.user ? this.user.screen_name_ui : '' }, htmlTitle () { return this.user ? this.user.name_html : '' diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue @@ -50,7 +50,6 @@ .Conversation { .conversation-status { - border-left: none; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: var(--border, $fallback--border); diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js @@ -194,11 +194,18 @@ const EmojiInput = { } }, methods: { + focusPickerInput () { + const pickerEl = this.$refs.picker.$el + if (!pickerEl) return + const pickerInput = pickerEl.querySelector('input') + if (pickerInput) pickerInput.focus() + }, triggerShowPicker () { this.showPicker = true this.$refs.picker.startEmojiLoad() this.$nextTick(() => { this.scrollIntoView() + this.focusPickerInput() }) // This temporarily disables "click outside" handler // since external trigger also means click originates @@ -214,6 +221,7 @@ const EmojiInput = { if (this.showPicker) { this.scrollIntoView() this.$refs.picker.startEmojiLoad() + this.$nextTick(this.focusPickerInput) } }, replace (replacement) { diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue @@ -9,6 +9,7 @@ <button v-if="!hideEmojiButton" class="button-unstyled emoji-picker-icon" + type="button" @click.prevent="togglePicker" > <FAIcon :icon="['far', 'smile-beam']" /> diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js @@ -116,8 +116,8 @@ export const suggestUsers = ({ dispatch, state }) => { return diff + nameAlphabetically + screenNameAlphabetically /* eslint-disable camelcase */ - }).map(({ screen_name, name, profile_image_url_original }) => ({ - displayText: screen_name, + }).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({ + displayText: screen_name_ui, detailText: name, imageUrl: profile_image_url_original, replacement: '@' + screen_name + ' ' diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue @@ -139,6 +139,11 @@ @import '../../_variables.scss'; .ExtraButtons { + /* override of popover internal stuff */ + .popover-trigger-button { + width: auto; + } + .popover-trigger { position: static; padding: 10px; diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -12,11 +12,11 @@ v-model="language" > <option - v-for="(langCode, i) in languageCodes" - :key="langCode" - :value="langCode" + v-for="lang in languages" + :key="lang.code" + :value="lang.code" > - {{ languageNames[i] }} + {{ lang.name }} </option> </select> <FAIcon @@ -29,6 +29,7 @@ <script> import languagesObject from '../../i18n/messages' +import localeService from '../../services/locale/locale.service.js' import ISO6391 from 'iso-639-1' import _ from 'lodash' import { library } from '@fortawesome/fontawesome-svg-core' @@ -42,12 +43,8 @@ library.add( export default { computed: { - languageCodes () { - return languagesObject.languages - }, - - languageNames () { - return _.map(this.languageCodes, this.getLanguageName) + languages () { + return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name)) }, language: { @@ -61,12 +58,13 @@ export default { methods: { getLanguageName (code) { const specialLanguageNames = { - 'ja': 'Japanese (日本語)', - 'ja_easy': 'Japanese (やさしいにほんご)', - 'zh': 'Simplified Chinese (简体中文)', - 'zh_Hant': 'Traditional Chinese (繁體中文)' + 'ja_easy': 'やさしいにほんご', + 'zh': '简体中文', + 'zh_Hant': '繁體中文' } - return specialLanguageNames[code] || ISO6391.getName(code) + const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code) + const browserLocale = localeService.internalToBrowserLocale(code) + return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1) } } } diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue @@ -73,11 +73,21 @@ } } +@keyframes media-fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + .modal-image { max-width: 90%; max-height: 90%; box-shadow: 0px 5px 15px 0 rgba(0, 0, 0, 0.5); image-orientation: from-image; // NOTE: only FF supports this + animation: 0.1s cubic-bezier(0.7, 0, 1, 0.6) media-fadein; } .modal-view-button-arrow { diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue @@ -25,6 +25,7 @@ <div> <button class="button-unstyled -link" + type="button" @click.prevent="requireTOTP" > {{ $t('login.enter_two_factor_code') }} @@ -32,6 +33,7 @@ <br> <button class="button-unstyled -link" + type="button" @click.prevent="abortMFA" > {{ $t('general.cancel') }} diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue @@ -27,6 +27,7 @@ <div> <button class="button-unstyled -link" + type="button" @click.prevent="requireRecovery" > {{ $t('login.enter_recovery_code') }} @@ -34,6 +35,7 @@ <br> <button class="button-unstyled -link" + type="button" @click.prevent="abortMFA" > {{ $t('general.cancel') }} diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue @@ -50,74 +50,74 @@ class="button-default dropdown-item" @click="toggleTag(tags.FORCE_NSFW)" > - {{ $t('user_card.admin_menu.force_nsfw') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_NSFW) }" /> + {{ $t('user_card.admin_menu.force_nsfw') }} </button> <button class="button-default dropdown-item" @click="toggleTag(tags.STRIP_MEDIA)" > - {{ $t('user_card.admin_menu.strip_media') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.STRIP_MEDIA) }" /> + {{ $t('user_card.admin_menu.strip_media') }} </button> <button class="button-default dropdown-item" @click="toggleTag(tags.FORCE_UNLISTED)" > - {{ $t('user_card.admin_menu.force_unlisted') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.FORCE_UNLISTED) }" /> + {{ $t('user_card.admin_menu.force_unlisted') }} </button> <button class="button-default dropdown-item" @click="toggleTag(tags.SANDBOX)" > - {{ $t('user_card.admin_menu.sandbox') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.SANDBOX) }" /> + {{ $t('user_card.admin_menu.sandbox') }} </button> <button v-if="user.is_local" class="button-default dropdown-item" @click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)" > - {{ $t('user_card.admin_menu.disable_remote_subscription') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_REMOTE_SUBSCRIPTION) }" /> + {{ $t('user_card.admin_menu.disable_remote_subscription') }} </button> <button v-if="user.is_local" class="button-default dropdown-item" @click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)" > - {{ $t('user_card.admin_menu.disable_any_subscription') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.DISABLE_ANY_SUBSCRIPTION) }" /> + {{ $t('user_card.admin_menu.disable_any_subscription') }} </button> <button v-if="user.is_local" class="button-default dropdown-item" @click="toggleTag(tags.QUARANTINE)" > - {{ $t('user_card.admin_menu.quarantine') }} <span class="menu-checkbox" :class="{ 'menu-checkbox-checked': hasTag(tags.QUARANTINE) }" /> + {{ $t('user_card.admin_menu.quarantine') }} </button> </span> </div> @@ -163,25 +163,6 @@ <style lang="scss"> @import '../../_variables.scss'; -.menu-checkbox { - float: right; - min-width: 22px; - max-width: 22px; - min-height: 22px; - max-height: 22px; - line-height: 22px; - text-align: center; - border-radius: 0px; - background-color: $fallback--fg; - background-color: var(--input, $fallback--fg); - box-shadow: 0px 0px 2px black inset; - box-shadow: var(--inputShadow); - - &.menu-checkbox-checked::after { - content: '✓'; - } -} - .moderation-tools-popover { height: 100%; .trigger { diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue @@ -11,7 +11,7 @@ > <small> <router-link :to="userProfileLink"> - {{ notification.from_profile.screen_name }} + {{ notification.from_profile.screen_name_ui }} </router-link> </small> <button @@ -54,14 +54,14 @@ <bdi v-if="!!notification.from_profile.name_html" class="username" - :title="'@'+notification.from_profile.screen_name" + :title="'@'+notification.from_profile.screen_name_ui" v-html="notification.from_profile.name_html" /> <!-- eslint-enable vue/no-v-html --> <span v-else class="username" - :title="'@'+notification.from_profile.screen_name" + :title="'@'+notification.from_profile.screen_name_ui" >{{ notification.from_profile.name }}</span> <span v-if="notification.type === 'like'"> <FAIcon @@ -152,7 +152,7 @@ :to="userProfileLink" class="follow-name" > - @{{ notification.from_profile.screen_name }} + @{{ notification.from_profile.screen_name_ui }} </router-link> <div v-if="notification.type === 'follow_request'" @@ -177,7 +177,7 @@ class="move-text" > <router-link :to="targetUserProfileLink"> - @{{ notification.target.screen_name }} + @{{ notification.target.screen_name_ui }} </router-link> </div> <template v-else> diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue @@ -58,7 +58,12 @@ {{ $t('polls.vote') }} </button> <div class="total"> - {{ totalVotesCount }} {{ $t("polls.votes") }}&nbsp;·&nbsp; + <template v-if="typeof poll.voters_count === 'number'"> + {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }}&nbsp;·&nbsp; + </template> + <template v-else> + {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }}&nbsp;·&nbsp; + </template> </div> <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'"> <Timeago diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue @@ -21,20 +21,17 @@ @keydown.enter.stop.prevent="nextOption(index)" > </div> - <div + <button v-if="options.length > 2" - class="icon-container" + class="delete-option button-unstyled -hover-highlight" + @click="deleteOption(index)" > - <FAIcon - icon="times" - class="delete" - @click="deleteOption(index)" - /> - </div> + <FAIcon icon="times" /> + </button> </div> - <a + <button v-if="options.length < maxOptions" - class="add-option faint" + class="add-option faint button-unstyled -hover-highlight" @click="addOption" > <FAIcon @@ -43,7 +40,7 @@ /> {{ $t("polls.add_option") }} - </a> + </button> <div class="poll-type-expiry"> <div class="poll-type" @@ -116,7 +113,6 @@ align-self: flex-start; padding-top: 0.25em; padding-left: 0.1em; - cursor: pointer; } .poll-option { @@ -135,19 +131,11 @@ } } - .icon-container { + .delete-option { // Hack: Move the icon over the input box width: 1.5em; margin-left: -1.5em; z-index: 1; - - .delete { - cursor: pointer; - - &:hover { - color: inherit; - } - } } .poll-type-expiry { @@ -163,6 +151,7 @@ border: none; box-shadow: none; background-color: transparent; + padding-right: 0.75em; } } diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js @@ -3,25 +3,32 @@ const Popover = { props: { // Action to trigger popover: either 'hover' or 'click' trigger: String, + // Either 'top' or 'bottom' placement: String, + // Takes object with properties 'x' and 'y', values of these can be // 'container' for using offsetParent as boundaries for either axis // or 'viewport' boundTo: Object, + // Takes a selector to use as a replacement for the parent container // for getting boundaries for x an y axis boundToSelector: String, + // Takes a top/bottom/left/right object, how much space to leave // between boundary and popover element margin: Object, + // Takes a x/y object and tells how many pixels to offset from // anchor point on either axis offset: Object, + // Replaces the classes you may want for the popover container. // Use 'popover-default' in addition to get the default popover // styles with your custom class. popoverClass: String, + // If true, subtract padding when calculating position for the popover, // use it when popover offset looks to be different on top vs bottom. removePadding: Boolean @@ -121,9 +128,12 @@ const Popover = { } }, showPopover () { - if (this.hidden) this.$emit('show') + const wasHidden = this.hidden this.hidden = false - this.$nextTick(this.updateStyles) + this.$nextTick(() => { + if (wasHidden) this.$emit('show') + this.updateStyles() + }) }, hidePopover () { if (!this.hidden) this.$emit('close') diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue @@ -6,6 +6,7 @@ <button ref="trigger" class="button-unstyled -fullwidth popover-trigger-button" + type="button" @click="onClick" > <slot name="trigger" /> @@ -81,10 +82,9 @@ .dropdown-item { line-height: 21px; - margin-right: 5px; overflow: auto; display: block; - padding: .25rem 1.0rem .25rem 1.5rem; + padding: .5em 0.75em; clear: both; font-weight: 400; text-align: inherit; @@ -100,10 +100,9 @@ --btnText: var(--popoverText, $fallback--text); &-icon { - padding-left: 0.5rem; - svg { - margin-right: 0.25rem; + width: 22px; + margin-right: 0.75rem; color: var(--menuPopoverIcon, $fallback--icon) } } @@ -122,6 +121,33 @@ } } + .menu-checkbox { + display: inline-block; + vertical-align: middle; + min-width: 22px; + max-width: 22px; + min-height: 22px; + max-height: 22px; + line-height: 22px; + text-align: center; + border-radius: 0px; + background-color: $fallback--fg; + background-color: var(--input, $fallback--fg); + box-shadow: 0px 0px 2px black inset; + box-shadow: var(--inputShadow); + margin-right: 0.75em; + + &.menu-checkbox-checked::after { + font-size: 1.25em; + content: '✓'; + } + + &.menu-checkbox-radio::after { + font-size: 2em; + content: '•'; + } + } + } } </style> diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js @@ -115,7 +115,7 @@ const PostStatusForm = { ? this.copyMessageScope : this.$store.state.users.currentUser.default_scope - const { postContentType: contentType } = this.$store.getters.mergedConfig + const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig return { dropFiles: [], @@ -126,7 +126,7 @@ const PostStatusForm = { newStatus: { spoilerText: this.subject || '', status: statusText, - nsfw: false, + nsfw: !!sensitiveByDefault, files: [], poll: {}, mediaDescriptions: {}, diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue @@ -302,11 +302,12 @@ :key="file.url" class="media-upload-wrapper" > - <FAIcon - class="fa-scale-110 fa-old-padding" - icon="times" + <button + class="button-unstyled hider" @click="removeMediaFile(file)" - /> + > + <FAIcon icon="times" /> + </button> <attachment :attachment="file" :set-media="() => $store.dispatch('setMedia', newStatus.files)" @@ -516,26 +517,11 @@ } .attachments .media-upload-wrapper { - padding: 0 0.5em; + position: relative; .attachment { margin: 0; padding: 0; - position: relative; - } - - .fa-scale-110 fa-old-padding { - position: absolute; - margin: 10px; - margin: .75em; - padding: .5em; - background: rgba(230,230,230,0.6); - z-index: 2; - color: black; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - font-weight: bold; - cursor: pointer; } } diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js @@ -23,6 +23,12 @@ const ReactButton = { this.$store.dispatch('reactWithEmoji', { id: this.status.id, emoji }) } close() + }, + focusInput () { + this.$nextTick(() => { + const input = this.$el.querySelector('input') + if (input) input.focus() + }) } }, computed: { diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue @@ -1,10 +1,12 @@ <template> <Popover trigger="click" + class="ReactButton" placement="top" :offset="{ y: 5 }" :bound-to="{ x: 'container' }" remove-padding + @show="focusInput" > <div slot="content" @@ -42,7 +44,7 @@ </div> <span slot="trigger" - class="ReactButton" + class="popover-trigger" :title="$t('tool_tip.add_reaction')" > <FAIcon @@ -58,62 +60,71 @@ <style lang="scss"> @import '../../_variables.scss'; -.reaction-picker-filter { - padding: 0.5em; - display: flex; - input { - flex: 1; +.ReactButton { + .reaction-picker-filter { + padding: 0.5em; + display: flex; + + input { + flex: 1; + } } -} -.reaction-picker-divider { - height: 1px; - width: 100%; - margin: 0.5em; - background-color: var(--border, $fallback--border); -} + .reaction-picker-divider { + height: 1px; + width: 100%; + margin: 0.5em; + background-color: var(--border, $fallback--border); + } -.reaction-picker { - width: 10em; - height: 9em; - font-size: 1.5em; - overflow-y: scroll; - display: flex; - flex-wrap: wrap; - padding: 0.5em; - text-align: center; - align-content: flex-start; - user-select: none; + .reaction-picker { + width: 10em; + height: 9em; + font-size: 1.5em; + overflow-y: scroll; + display: flex; + flex-wrap: wrap; + padding: 0.5em; + text-align: center; + align-content: flex-start; + user-select: none; - mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, - linear-gradient(to bottom, white 0, transparent 100%) top no-repeat, - linear-gradient(to top, white, white); - transition: mask-size 150ms; - mask-size: 100% 20px, 100% 20px, auto; - // Autoprefixed seem to ignore this one, and also syntax is different - -webkit-mask-composite: xor; - mask-composite: exclude; + mask: linear-gradient(to top, white 0, transparent 100%) bottom no-repeat, + linear-gradient(to bottom, white 0, transparent 100%) top no-repeat, + linear-gradient(to top, white, white); + transition: mask-size 150ms; + mask-size: 100% 20px, 100% 20px, auto; - .emoji-button { - cursor: pointer; + /* Autoprefixed seem to ignore this one, and also syntax is different */ + -webkit-mask-composite: xor; + mask-composite: exclude; - flex-basis: 20%; - line-height: 1.5em; - align-content: center; + .emoji-button { + cursor: pointer; - &:hover { - transform: scale(1.25); + flex-basis: 20%; + line-height: 1.5em; + align-content: center; + + &:hover { + transform: scale(1.25); + } } } -} -.ReactButton { - padding: 10px; - margin: -10px; + /* override of popover internal stuff */ + .popover-trigger-button { + width: auto; + } + + .popover-trigger { + padding: 10px; + margin: -10px; - &:hover .svg-inline--fa { - color: $fallback--text; - color: var(--text, $fallback--text); + &:hover .svg-inline--fa { + color: $fallback--text; + color: var(--text, $fallback--text); + } } } diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js @@ -10,7 +10,8 @@ const registration = { fullname: '', username: '', password: '', - confirm: '' + confirm: '', + reason: '' }, captcha: {} }), @@ -24,7 +25,8 @@ const registration = { confirm: { required, sameAsPassword: sameAs('password') - } + }, + reason: { required: requiredIf(() => this.accountApprovalRequired) } } } }, @@ -38,7 +40,10 @@ const registration = { computed: { token () { return this.$route.params.token }, bioPlaceholder () { - return this.$t('registration.bio_placeholder').replace(/\s*\n\s*/g, ' \n') + return this.replaceNewlines(this.$t('registration.bio_placeholder')) + }, + reasonPlaceholder () { + return this.replaceNewlines(this.$t('registration.reason_placeholder')) }, ...mapState({ registrationOpen: (state) => state.instance.registrationOpen, @@ -46,7 +51,8 @@ const registration = { isPending: (state) => state.users.signUpPending, serverValidationErrors: (state) => state.users.signUpErrors, termsOfService: (state) => state.instance.tos, - accountActivationRequired: (state) => state.instance.accountActivationRequired + accountActivationRequired: (state) => state.instance.accountActivationRequired, + accountApprovalRequired: (state) => state.instance.accountApprovalRequired }) }, methods: { @@ -73,6 +79,9 @@ const registration = { }, setCaptcha () { this.getCaptcha().then(cpt => { this.captcha = cpt }) + }, + replaceNewlines (str) { + return str.replace(/\s*\n\s*/g, ' \n') } } } diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue @@ -163,6 +163,23 @@ </div> <div + v-if="accountApprovalRequired" + class="form-group" + > + <label + class="form--label" + for="reason" + >{{ $t('registration.reason') }}</label> + <textarea + id="reason" + v-model="user.reason" + :disabled="isPending" + class="form-control" + :placeholder="reasonPlaceholder" + /> + </div> + + <div v-if="captcha.type != 'none'" id="captcha-group" class="form-group" diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue @@ -8,6 +8,7 @@ class="button-unstyled scope" :class="css.direct" :title="$t('post_status.scope.direct')" + type="button" @click="changeVis('direct')" > <FAIcon @@ -20,6 +21,7 @@ class="button-unstyled scope" :class="css.private" :title="$t('post_status.scope.private')" + type="button" @click="changeVis('private')" > <FAIcon @@ -32,6 +34,7 @@ class="button-unstyled scope" :class="css.unlisted" :title="$t('post_status.scope.unlisted')" + type="button" @click="changeVis('unlisted')" > <FAIcon @@ -44,6 +47,7 @@ class="button-unstyled scope" :class="css.public" :title="$t('post_status.scope.public')" + type="button" @click="changeVis('public')" > <FAIcon diff --git a/src/components/search/search.vue b/src/components/search/search.vue @@ -15,6 +15,7 @@ > <button class="btn button-default search-button" + type="submit" @click="newQuery(searchTerm)" > <FAIcon icon="search" /> diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue @@ -7,6 +7,7 @@ v-if="hidden" class="button-unstyled nav-icon" :title="$t('nav.search')" + type="button" @click.prevent.stop="toggleHidden" > <FAIcon @@ -27,6 +28,7 @@ > <button class="button-default search-button" + type="submit" @click="find(searchTerm)" > <FAIcon @@ -36,6 +38,7 @@ </button> <button class="button-unstyled cancel-search" + type="button" @click.prevent.stop="toggleHidden" > <FAIcon diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue @@ -0,0 +1,57 @@ +<template> + <label + class="BooleanSetting" + > + <Checkbox + :checked="state" + :disabled="disabled" + @change="update" + > + <span + v-if="!!$slots.default" + class="label" + > + <slot /> + </span> + <ModifiedIndicator :changed="isChanged" /> + </Checkbox> + </label> +</template> + +<script> +import { get, set } from 'lodash' +import Checkbox from 'src/components/checkbox/checkbox.vue' +import ModifiedIndicator from './modified_indicator.vue' +export default { + components: { + Checkbox, + ModifiedIndicator + }, + props: [ + 'path', + 'disabled' + ], + computed: { + pathDefault () { + const [firstSegment, ...rest] = this.path.split('.') + return [firstSegment + 'DefaultValue', ...rest].join('.') + }, + state () { + return get(this.$parent, this.path) + }, + isChanged () { + return get(this.$parent, this.path) !== get(this.$parent, this.pathDefault) + } + }, + methods: { + update (e) { + set(this.$parent, this.path, e) + } + } +} +</script> + +<style lang="scss"> +.BooleanSetting { +} +</style> diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue @@ -0,0 +1,51 @@ +<template> + <span + v-if="changed" + class="ModifiedIndicator" + > + <Popover + trigger="hover" + > + <span slot="trigger"> + &nbsp; + <FAIcon + icon="wrench" + /> + </span> + <div + slot="content" + class="modified-tooltip" + > + {{ $t('settings.setting_changed') }} + </div> + </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"> +.ModifiedIndicator { + display: inline-block; + position: relative; + + .modified-tooltip { + margin: 0.5em 1em; + min-width: 10em; + text-align: center; + } +} +</style> diff --git a/src/components/settings_modal/helpers/shared_computed_object.js b/src/components/settings_modal/helpers/shared_computed_object.js @@ -1,29 +1,15 @@ -import { - instanceDefaultProperties, - multiChoiceProperties, - defaultState as configDefaultState -} from 'src/modules/config.js' +import { defaultState as configDefaultState } from 'src/modules/config.js' const SharedComputedObject = () => ({ user () { return this.$store.state.users.currentUser }, - // Getting localized values for instance-default properties - ...instanceDefaultProperties - .filter(key => multiChoiceProperties.includes(key)) + // Getting values for default properties + ...Object.keys(configDefaultState) .map(key => [ key + 'DefaultValue', function () { - return this.$store.getters.instanceDefaultConfig[key] - } - ]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), - ...instanceDefaultProperties - .filter(key => !multiChoiceProperties.includes(key)) - .map(key => [ - key + 'LocalizedValue', - function () { - return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key]) + return this.$store.getters.defaultConfig[key] } ]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js @@ -1,5 +1,5 @@ import { filter, trim } from 'lodash' -import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -18,7 +18,7 @@ const FilteringTab = { } }, components: { - Checkbox + BooleanSetting }, computed: { ...SharedComputedObject(), diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue @@ -5,34 +5,34 @@ <span class="label">{{ $t('settings.notification_visibility') }}</span> <ul class="option-list"> <li> - <Checkbox v-model="notificationVisibility.likes"> + <BooleanSetting path="notificationVisibility.likes"> {{ $t('settings.notification_visibility_likes') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="notificationVisibility.repeats"> + <BooleanSetting path="notificationVisibility.repeats"> {{ $t('settings.notification_visibility_repeats') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="notificationVisibility.follows"> + <BooleanSetting path="notificationVisibility.follows"> {{ $t('settings.notification_visibility_follows') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="notificationVisibility.mentions"> + <BooleanSetting path="notificationVisibility.mentions"> {{ $t('settings.notification_visibility_mentions') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="notificationVisibility.moves"> + <BooleanSetting path="notificationVisibility.moves"> {{ $t('settings.notification_visibility_moves') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="notificationVisibility.emojiReactions"> + <BooleanSetting path="notificationVisibility.emojiReactions"> {{ $t('settings.notification_visibility_emoji_reactions') }} - </Checkbox> + </BooleanSetting> </li> </ul> </div> @@ -60,14 +60,14 @@ </label> </div> <div> - <Checkbox v-model="hidePostStats"> - {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="hidePostStats"> + {{ $t('settings.hide_post_stats') }} + </BooleanSetting> </div> <div> - <Checkbox v-model="hideUserStats"> - {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="hideUserStats"> + {{ $t('settings.hide_user_stats') }} + </BooleanSetting> </div> </div> <div class="setting-item"> @@ -75,14 +75,14 @@ <p>{{ $t('settings.filtering_explanation') }}</p> <textarea id="muteWords" - class="resize-height" v-model="muteWordsString" + class="resize-height" /> </div> <div> - <Checkbox v-model="hideFilteredStatuses"> - {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="hideFilteredStatuses"> + {{ $t('settings.hide_filtered_statuses') }} + </BooleanSetting> </div> </div> </div> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js @@ -1,4 +1,4 @@ -import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' @@ -26,7 +26,7 @@ const GeneralTab = { } }, components: { - Checkbox, + BooleanSetting, InterfaceLanguageSwitcher }, computed: { diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue @@ -7,14 +7,14 @@ <interface-language-switcher /> </li> <li v-if="instanceSpecificPanelPresent"> - <Checkbox v-model="hideISP"> + <BooleanSetting path="hideISP"> {{ $t('settings.hide_isp') }} - </Checkbox> + </BooleanSetting> </li> <li v-if="instanceWallpaperUsed"> - <Checkbox v-model="hideInstanceWallpaper"> + <BooleanSetting path="hideInstanceWallpaper"> {{ $t('settings.hide_wallpaper') }} - </Checkbox> + </BooleanSetting> </li> </ul> </div> @@ -22,51 +22,51 @@ <h2>{{ $t('nav.timeline') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="hideMutedPosts"> - {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="hideMutedPosts"> + {{ $t('settings.hide_muted_posts') }} + </BooleanSetting> </li> <li> - <Checkbox v-model="collapseMessageWithSubject"> - {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="collapseMessageWithSubject"> + {{ $t('settings.collapse_subject') }} + </BooleanSetting> </li> <li> - <Checkbox v-model="streaming"> + <BooleanSetting path="streaming"> {{ $t('settings.streaming') }} - </Checkbox> + </BooleanSetting> <ul class="setting-list suboptions" :class="[{disabled: !streaming}]" > <li> - <Checkbox - v-model="pauseOnUnfocused" + <BooleanSetting + path="pauseOnUnfocused" :disabled="!streaming" > {{ $t('settings.pause_on_unfocused') }} - </Checkbox> + </BooleanSetting> </li> </ul> </li> <li> - <Checkbox v-model="useStreamingApi"> + <BooleanSetting path="useStreamingApi"> {{ $t('settings.useStreamingApi') }} <br> <small> {{ $t('settings.useStreamingApiWarning') }} </small> - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="emojiReactionsOnTimeline"> + <BooleanSetting path="emojiReactionsOnTimeline"> {{ $t('settings.emoji_reactions_on_timeline') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="virtualScrolling"> + <BooleanSetting path="virtualScrolling"> {{ $t('settings.virtual_scrolling') }} - </Checkbox> + </BooleanSetting> </li> </ul> </div> @@ -75,14 +75,14 @@ <h2>{{ $t('settings.composing') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="scopeCopy"> - {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="scopeCopy"> + {{ $t('settings.scope_copy') }} + </BooleanSetting> </li> <li> - <Checkbox v-model="alwaysShowSubjectInput"> - {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="alwaysShowSubjectInput"> + {{ $t('settings.subject_input_always_show') }} + </BooleanSetting> </li> <li> <div> @@ -143,19 +143,24 @@ </div> </li> <li> - <Checkbox v-model="minimalScopesMode"> - {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="minimalScopesMode"> + {{ $t('settings.minimal_scopes_mode') }} + </BooleanSetting> </li> <li> - <Checkbox v-model="autohideFloatingPostButton"> + <BooleanSetting path="sensitiveByDefault"> + {{ $t('settings.sensitive_by_default') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="autohideFloatingPostButton"> {{ $t('settings.autohide_floating_post_button') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="padEmoji"> + <BooleanSetting path="padEmoji"> {{ $t('settings.pad_emoji') }} - </Checkbox> + </BooleanSetting> </li> </ul> </div> @@ -164,14 +169,14 @@ <h2>{{ $t('settings.attachments') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="hideAttachments"> + <BooleanSetting path="hideAttachments"> {{ $t('settings.hide_attachments_in_tl') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="hideAttachmentsInConv"> + <BooleanSetting path="hideAttachmentsInConv"> {{ $t('settings.hide_attachments_in_convo') }} - </Checkbox> + </BooleanSetting> </li> <li> <label for="maxThumbnails"> @@ -179,7 +184,7 @@ </label> <input id="maxThumbnails" - v-model.number="maxThumbnails" + path.number="maxThumbnails" class="number-input" type="number" min="0" @@ -187,48 +192,48 @@ > </li> <li> - <Checkbox v-model="hideNsfw"> + <BooleanSetting path="hideNsfw"> {{ $t('settings.nsfw_clickthrough') }} - </Checkbox> + </BooleanSetting> </li> <ul class="setting-list suboptions"> <li> - <Checkbox - v-model="preloadImage" + <BooleanSetting + path="preloadImage" :disabled="!hideNsfw" > {{ $t('settings.preload_images') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox - v-model="useOneClickNsfw" + <BooleanSetting + path="useOneClickNsfw" :disabled="!hideNsfw" > {{ $t('settings.use_one_click_nsfw') }} - </Checkbox> + </BooleanSetting> </li> </ul> <li> - <Checkbox v-model="stopGifs"> + <BooleanSetting path="stopGifs"> {{ $t('settings.stop_gifs') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="loopVideo"> + <BooleanSetting path="loopVideo"> {{ $t('settings.loop_video') }} - </Checkbox> + </BooleanSetting> <ul class="setting-list suboptions" :class="[{disabled: !streaming}]" > <li> - <Checkbox - v-model="loopVideoSilentOnly" + <BooleanSetting + path="loopVideoSilentOnly" :disabled="!loopVideo || !loopSilentAvailable" > {{ $t('settings.loop_video_silent_only') }} - </Checkbox> + </BooleanSetting> <div v-if="!loopSilentAvailable" class="unavailable" @@ -239,14 +244,14 @@ </ul> </li> <li> - <Checkbox v-model="playVideosInModal"> + <BooleanSetting path="playVideosInModal"> {{ $t('settings.play_videos_in_modal') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="useContainFit"> + <BooleanSetting path="useContainFit"> {{ $t('settings.use_contain_fit') }} - </Checkbox> + </BooleanSetting> </li> </ul> </div> @@ -255,9 +260,9 @@ <h2>{{ $t('settings.notifications') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="webPushNotifications"> + <BooleanSetting path="webPushNotifications"> {{ $t('settings.enable_web_push_notifications') }} - </Checkbox> + </BooleanSetting> </li> </ul> </div> @@ -266,9 +271,9 @@ <h2>{{ $t('settings.fun') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="greentext"> - {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="greentext"> + {{ $t('settings.greentext') }} + </BooleanSetting> </li> </ul> </div> diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss @@ -111,16 +111,17 @@ .profile-fields { display: flex; - &>.emoji-input { + & > .emoji-input { flex: 1 1 auto; - margin: 0 .2em .5em; + margin: 0 0.2em 0.5em; min-width: 0; } - &>.icon-container { + .delete-field { width: 20px; align-self: center; - margin: 0 .2em .5em; + margin: 0 0.2em 0.5em; + padding: 0 0.5em; } } } diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue @@ -124,24 +124,24 @@ :placeholder="$t('settings.profile_fields.value')" > </EmojiInput> - <div - class="icon-container" + <button + class="delete-field button-unstyled -hover-highlight" + @click="deleteField(i)" > <FAIcon v-show="newFields.length > 1" icon="times" - @click="deleteField(i)" /> - </div> + </button> </div> - <a + <button v-if="newFields.length < maxFields" - class="add-field faint" + class="add-field faint button-unstyled -hover-highlight" @click="addField" > <FAIcon icon="plus" /> {{ $t("settings.profile_fields.add_field") }} - </a> + </button> </div> <p> <Checkbox v-model="bot"> diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js @@ -1,6 +1,7 @@ import ProgressButton from 'src/components/progress_button/progress_button.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import Mfa from './mfa.vue' +import localeService from 'src/services/locale/locale.service.js' const SecurityTab = { data () { @@ -37,7 +38,7 @@ const SecurityTab = { return { id: oauthToken.id, appName: oauthToken.app_name, - validUntil: new Date(oauthToken.valid_until).toLocaleDateString() + validUntil: new Date(oauthToken.valid_until).toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale)) } }) } diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue @@ -109,7 +109,7 @@ v-if="chat" @click="toggleDrawer" > - <router-link :to="{ name: 'chat' }"> + <router-link :to="{ name: 'chat-panel' }"> <FAIcon fixed-width class="fa-scale-110 fa-old-padding" diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js @@ -1,4 +1,6 @@ import map from 'lodash/map' +import groupBy from 'lodash/groupBy' +import { mapGetters, mapState } from 'vuex' import BasicUserCard from '../basic_user_card/basic_user_card.vue' const StaffPanel = { @@ -10,9 +12,21 @@ const StaffPanel = { BasicUserCard }, computed: { - staffAccounts () { - return map(this.$store.state.instance.staffAccounts, nickname => this.$store.getters.findUser(nickname)).filter(_ => _) - } + groupedStaffAccounts () { + const staffAccounts = map(this.staffAccounts, this.findUser).filter(_ => _) + const groupedStaffAccounts = groupBy(staffAccounts, 'role') + + return [ + { role: 'admin', users: groupedStaffAccounts['admin'] }, + { role: 'moderator', users: groupedStaffAccounts['moderator'] } + ].filter(group => group.users) + }, + ...mapGetters([ + 'findUser' + ]), + ...mapState({ + staffAccounts: state => state.instance.staffAccounts + }) } } diff --git a/src/components/staff_panel/staff_panel.vue b/src/components/staff_panel/staff_panel.vue @@ -7,11 +7,18 @@ </div> </div> <div class="panel-body"> - <basic-user-card - v-for="user in staffAccounts" - :key="user.screen_name" - :user="user" - /> + <div + v-for="group in groupedStaffAccounts" + :key="group.role" + class="staff-group" + > + <h4>{{ $t('general.role.' + group.role) }}</h4> + <basic-user-card + v-for="user in group.users" + :key="user.screen_name" + :user="user" + /> + </div> </div> </div> </div> @@ -20,4 +27,14 @@ <script src="./staff_panel.js" ></script> <style lang="scss"> + +.staff-group { + padding-left: 1em; + padding-top: 1em; + + .basic-user-card { + padding-left: 0; + } +} + </style> diff --git a/src/components/status/status.js b/src/components/status/status.js @@ -136,7 +136,7 @@ const Status = { } }, retweet () { return !!this.statusoid.retweeted_status }, - retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name }, + retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui }, retweeterHtml () { return this.statusoid.user.name_html }, retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) }, status () { @@ -157,6 +157,7 @@ const Status = { return muteWordHits(this.status, this.muteWords) }, muted () { + if (this.statusoid.user.id === this.currentUser.id) return false const { status } = this const { reblog } = status const relationship = this.$store.getters.relationship(status.user.id) @@ -215,7 +216,7 @@ const Status = { return this.status.in_reply_to_screen_name } else { const user = this.$store.getters.findUser(this.status.in_reply_to_user_id) - return user && user.screen_name + return user && user.screen_name_ui } }, replySubject () { diff --git a/src/components/status/status.vue b/src/components/status/status.vue @@ -26,7 +26,7 @@ icon="retweet" /> <router-link :to="userProfileLink"> - {{ status.user.screen_name }} + {{ status.user.screen_name_ui }} </router-link> </small> <small @@ -156,10 +156,10 @@ </h4> <router-link class="account-name" - :title="status.user.screen_name" + :title="status.user.screen_name_ui" :to="userProfileLink" > - {{ status.user.screen_name }} + {{ status.user.screen_name_ui }} </router-link> <img v-if="!!(status.user && status.user.favicon)" diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js @@ -93,7 +93,9 @@ export default Vue.component('tab-switcher', { <button disabled={slot.data.attrs.disabled} onClick={this.clickTab(index)} - class={classesTab.join(' ')}> + class={classesTab.join(' ')} + type="button" + > <img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/> {slot.data.attrs.label ? '' : slot.data.attrs.label} </button> diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue @@ -9,6 +9,7 @@ <script> import * as DateUtils from 'src/services/date_utils/date_utils.js' +import localeService from 'src/services/locale/locale.service.js' export default { name: 'Timeago', @@ -21,9 +22,10 @@ export default { }, computed: { localeDateString () { + const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale) return typeof this.time === 'string' - ? new Date(Date.parse(this.time)).toLocaleString() - : this.time.toLocaleString() + ? new Date(Date.parse(this.time)).toLocaleString(browserLocale) + : this.time.toLocaleString(browserLocale) } }, created () { diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js @@ -2,12 +2,14 @@ import Status from '../status/status.vue' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import Conversation from '../conversation/conversation.vue' import TimelineMenu from '../timeline_menu/timeline_menu.vue' +import TimelineQuickSettings from './timeline_quick_settings.vue' import { debounce, throttle, keyBy } from 'lodash' import { library } from '@fortawesome/fontawesome-svg-core' -import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' +import { faCircleNotch, faCog } from '@fortawesome/free-solid-svg-icons' library.add( - faCircleNotch + faCircleNotch, + faCog ) export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => { @@ -47,7 +49,8 @@ const Timeline = { components: { Status, Conversation, - TimelineMenu + TimelineMenu, + TimelineQuickSettings }, computed: { newStatusCount () { diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue @@ -16,6 +16,7 @@ > {{ $t('timeline.up_to_date') }} </div> + <TimelineQuickSettings v-if="!embedded" /> </div> <div :class="classes.body"> <div @@ -103,9 +104,12 @@ max-width: 100%; flex-wrap: nowrap; align-items: center; + position: relative; + .loadmore-button { flex-shrink: 0; } + .loadmore-text { flex-shrink: 0; line-height: 1em; diff --git a/src/components/timeline/timeline_quick_settings.js b/src/components/timeline/timeline_quick_settings.js @@ -0,0 +1,63 @@ +import Popover from '../popover/popover.vue' +import BooleanSetting from '../settings_modal/helpers/boolean_setting.vue' +import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faFilter, faFont, faWrench } from '@fortawesome/free-solid-svg-icons' + +library.add( + faFilter, + faFont, + faWrench +) + +const TimelineQuickSettings = { + components: { + Popover, + BooleanSetting + }, + methods: { + setReplyVisibility (visibility) { + this.$store.dispatch('setOption', { name: 'replyVisibility', value: visibility }) + this.$store.dispatch('queueFlushAll') + }, + openTab (tab) { + this.$store.dispatch('openSettingsModalTab', tab) + } + }, + computed: { + ...mapGetters(['mergedConfig']), + loggedIn () { + return !!this.$store.state.users.currentUser + }, + replyVisibilitySelf: { + get () { return this.mergedConfig.replyVisibility === 'self' }, + set () { this.setReplyVisibility('self') } + }, + replyVisibilityFollowing: { + get () { return this.mergedConfig.replyVisibility === 'following' }, + set () { this.setReplyVisibility('following') } + }, + replyVisibilityAll: { + get () { return this.mergedConfig.replyVisibility === 'all' }, + set () { this.setReplyVisibility('all') } + }, + hideMedia: { + get () { return this.mergedConfig.hideAttachments || this.mergedConfig.hideAttachmentsInConv }, + set () { + const value = !this.hideMedia + this.$store.dispatch('setOption', { name: 'hideAttachments', value }) + this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value }) + } + }, + hideMutedPosts: { + get () { return this.mergedConfig.hideMutedPosts || this.mergedConfig.hideFilteredStatuses }, + set () { + const value = !this.hideMutedPosts + this.$store.dispatch('setOption', { name: 'hideMutedPosts', value }) + this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value }) + } + } + } +} + +export default TimelineQuickSettings diff --git a/src/components/timeline/timeline_quick_settings.vue b/src/components/timeline/timeline_quick_settings.vue @@ -0,0 +1,107 @@ +<template> + <Popover + trigger="click" + class="TimelineQuickSettings" + :bound-to="{ x: 'container' }" + > + <div + slot="content" + class="timeline-settings-menu dropdown-menu" + > + <div v-if="loggedIn"> + <button + class="button-default dropdown-item" + @click="replyVisibilityAll = true" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-radio': replyVisibilityAll }" + />{{ $t('settings.reply_visibility_all') }} + </button> + <button + class="button-default dropdown-item" + @click="replyVisibilityFollowing = true" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-radio': replyVisibilityFollowing }" + />{{ $t('settings.reply_visibility_following_short') }} + </button> + <button + class="button-default dropdown-item" + @click="replyVisibilitySelf = true" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-radio': replyVisibilitySelf }" + />{{ $t('settings.reply_visibility_self_short') }} + </button> + <div + role="separator" + class="dropdown-divider" + /> + </div> + <button + class="button-default dropdown-item" + @click="hideMedia = !hideMedia" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': hideMedia }" + />{{ $t('settings.hide_media_previews') }} + </button> + <button + class="button-default dropdown-item" + @click="hideMutedPosts = !hideMutedPosts" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': hideMutedPosts }" + />{{ $t('settings.hide_all_muted_posts') }} + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click="openTab('filtering')" + > + <FAIcon icon="font" />{{ $t('settings.word_filter') }} + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click="openTab('general')" + > + <FAIcon icon="wrench" />{{ $t('settings.more_settings') }} + </button> + </div> + <div slot="trigger"> + <FAIcon icon="filter" /> + </div> + </Popover> +</template> + +<script src="./timeline_quick_settings.js"></script> + +<style lang="scss"> + +.TimelineQuickSettings { + align-self: stretch; + + > button { + font-size: 1.2em; + padding-left: 0.7em; + padding-right: 0.2em; + line-height: 100%; + height: 100%; + } + + .dropdown-item { + margin: 0; + } + + .timeline-settings-menu { + display: flex; + min-width: 12em; + flex-direction: column; + } +} + +</style> diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue @@ -2,8 +2,8 @@ <StillImage v-if="user" class="Avatar" - :alt="user.screen_name" - :title="user.screen_name" + :alt="user.screen_name_ui" + :title="user.screen_name_ui" :src="imgSrc(user.profile_image_url_original)" :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :image-load-error="imageLoadError" diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue @@ -73,23 +73,23 @@ <div class="bottom-line"> <router-link class="user-screen-name" - :title="user.screen_name" + :title="user.screen_name_ui" :to="userProfileLink(user)" > - @{{ user.screen_name }} + @{{ user.screen_name_ui }} </router-link> <template v-if="!hideBio"> <span v-if="!!visibleRole" class="alert user-role" > - {{ visibleRole }} + {{ $t(`general.role.${visibleRole}`) }} </span> <span v-if="user.bot" class="alert user-role" > - bot + {{ $t('user_card.bot') }} </span> </template> <span v-if="user.locked"> @@ -141,10 +141,10 @@ v-model="userHighlightType" class="userHighlightSel" > - <option value="disabled">No highlight</option> - <option value="solid">Solid bg</option> - <option value="striped">Striped bg</option> - <option value="side">Side stripe</option> + <option value="disabled">{{ $t('user_card.highlight.disabled') }}</option> + <option value="solid">{{ $t('user_card.highlight.solid') }}</option> + <option value="striped">{{ $t('user_card.highlight.striped') }}</option> + <option value="side">{{ $t('user_card.highlight.side') }}</option> </select> <FAIcon class="select-down-icon" @@ -507,7 +507,6 @@ .user-role { flex: none; - text-transform: capitalize; color: $fallback--text; color: var(--alertNeutralText, $fallback--text); background-color: $fallback--fg; diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue @@ -26,7 +26,7 @@ <!-- eslint-disable vue/no-v-html --> <span v-html="user.name_html" /> <!-- eslint-enable vue/no-v-html --> - <span class="user-list-screen-name">{{ user.screen_name }}</span> + <span class="user-list-screen-name">{{ user.screen_name_ui }}</span> </div> </div> </div> diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -6,7 +6,7 @@ <div class="user-reporting-panel panel"> <div class="panel-heading"> <div class="title"> - {{ $t('user_reporting.title', [user.screen_name]) }} + {{ $t('user_reporting.title', [user.screen_name_ui]) }} </div> </div> <div class="panel-body"> diff --git a/src/i18n/en.json b/src/i18n/en.json @@ -75,7 +75,11 @@ "confirm": "Confirm", "verify": "Verify", "close": "Close", - "peek": "Peek" + "peek": "Peek", + "role": { + "admin": "Admin", + "moderator": "Moderator" + } }, "image_cropper": { "crop_picture": "Crop picture", @@ -148,6 +152,8 @@ "add_option": "Add Option", "option": "Option", "votes": "votes", + "people_voted_count": "{count} person voted | {count} people voted", + "votes_count": "{count} vote | {count} votes", "vote": "Vote", "type": "Poll type", "single_choice": "Single choice", @@ -222,6 +228,8 @@ "username_placeholder": "e.g. lain", "fullname_placeholder": "e.g. Lain Iwakura", "bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m an anime girl living in suburban Japan. You may know me from the Wired.", + "reason": "Reason to register", + "reason_placeholder": "This instance approves registrations manually.\nLet the administration know why you want to register.", "validations": { "username_required": "cannot be left blank", "fullname_required": "cannot be left blank", @@ -242,6 +250,7 @@ "settings": { "app_name": "App name", "security": "Security", + "setting_changed": "Setting is different from default", "enter_current_password_to_confirm": "Enter your current password to confirm your identity", "mfa": { "otp": "OTP", @@ -316,6 +325,7 @@ "export_theme": "Save preset", "filtering": "Filtering", "filtering_explanation": "All statuses containing these words will be muted, one per line", + "word_filter": "Word filter", "follow_export": "Follow export", "follow_export_button": "Export your follows to a csv file", "follow_import": "Follow import", @@ -326,7 +336,9 @@ "general": "General", "hide_attachments_in_convo": "Hide attachments in conversations", "hide_attachments_in_tl": "Hide attachments in timeline", + "hide_media_previews": "Hide media previews", "hide_muted_posts": "Hide posts of muted users", + "hide_all_muted_posts": "Hide muted posts", "max_thumbnails": "Maximum amount of thumbnails per post", "hide_isp": "Hide instance-specific panel", "hide_wallpaper": "Hide instance wallpaper", @@ -396,6 +408,8 @@ "reply_visibility_all": "Show all replies", "reply_visibility_following": "Only show replies directed at me or users I'm following", "reply_visibility_self": "Only show replies directed at me", + "reply_visibility_following_short": "Show replies to my follows", + "reply_visibility_self_short": "Show replies to self only", "autohide_floating_post_button": "Automatically hide New Post button (mobile)", "saving_err": "Error saving settings", "saving_ok": "Settings saved", @@ -420,6 +434,7 @@ "subject_line_mastodon": "Like mastodon: copy as is", "subject_line_noop": "Do not copy", "post_status_content_type": "Post status content type", + "sensitive_by_default": "Mark posts as sensitive by default", "stop_gifs": "Play-on-hover GIFs", "streaming": "Enable automatic streaming of new posts when scrolled to the top", "user_mutes": "Users", @@ -449,6 +464,7 @@ "notification_mutes": "To stop receiving notifications from a specific user, use a mute.", "notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.", "enable_web_push_notifications": "Enable web push notifications", + "more_settings": "More settings", "style": { "switcher": { "keep_color": "Keep colors", @@ -714,6 +730,7 @@ "mute_progress": "Muting…", "hide_repeats": "Hide repeats", "show_repeats": "Show repeats", + "bot": "Bot", "admin_menu": { "moderation": "Moderation", "grant_admin": "Grant Admin", @@ -732,6 +749,12 @@ "quarantine": "Disallow user posts from federating", "delete_user": "Delete user", "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone." + }, + "highlight": { + "disabled": "No highlight", + "solid": "Solid bg", + "striped": "Striped bg", + "side": "Side stripe" } }, "user_profile": { diff --git a/src/i18n/eo.json b/src/i18n/eo.json @@ -35,7 +35,11 @@ "retry": "Reprovi", "error_retry": "Bonvolu reprovi", "loading": "Enlegante…", - "peek": "Antaŭmontri" + "peek": "Antaŭmontri", + "role": { + "moderator": "Reguligisto", + "admin": "Administranto" + } }, "image_cropper": { "crop_picture": "Tondi bildon", @@ -365,7 +369,8 @@ "post": "Afiŝoj/Priskriboj de uzantoj", "alert_neutral": "Neŭtrala", "alert_warning": "Averto", - "toggled": "Ŝaltita" + "toggled": "Ŝaltita", + "wallpaper": "Fonbildo" }, "radii": { "_tab_label": "Rondeco" @@ -516,7 +521,9 @@ "mute_import_error": "Eraris enporto de silentigoj", "mute_import": "Enporto de silentigoj", "mute_export_button": "Elportu viajn silentigojn al CSV-dosiero", - "mute_export": "Elporto de silentigoj" + "mute_export": "Elporto de silentigoj", + "hide_wallpaper": "Kaŝi fonbildon de nodo", + "setting_changed": "Agordo malsamas de la implicita" }, "timeline": { "collapse": "Maletendi", @@ -586,7 +593,8 @@ "show_repeats": "Montri ripetojn", "hide_repeats": "Kaŝi ripetojn", "unsubscribe": "Ne ricevi sciigojn", - "subscribe": "Ricevi sciigojn" + "subscribe": "Ricevi sciigojn", + "bot": "Roboto" }, "user_profile": { "timeline_title": "Historio de uzanto", @@ -612,7 +620,8 @@ "error": { "base": "Alŝuto malsukcesis.", "file_too_big": "Dosiero estas tro granda [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Reprovu pli poste" + "default": "Reprovu pli poste", + "message": "Malsukcesis alŝuto: {0}" }, "file_size_units": { "B": "B", @@ -645,7 +654,9 @@ "votes": "voĉoj", "option": "Elekteblo", "add_option": "Aldoni elekteblon", - "add_poll": "Aldoni enketon" + "add_poll": "Aldoni enketon", + "votes_count": "{count} voĉdono | {count} voĉdonoj", + "people_voted_count": "{count} persono voĉdonis | {count} personoj voĉdonis" }, "importer": { "error": "Eraris enporto de ĉi tiu dosiero.", @@ -732,7 +743,9 @@ "repeats": "Ripetoj", "favorites": "Ŝatoj", "status_deleted": "Ĉi tiu afiŝo foriĝis", - "nsfw": "Konsterna" + "nsfw": "Konsterna", + "expand": "Etendi", + "external_source": "Ekstera fonto" }, "time": { "years_short": "{0}j", diff --git a/src/i18n/es.json b/src/i18n/es.json @@ -562,7 +562,8 @@ "mute_import": "Importar silenciados", "mute_export_button": "Exportar los silenciados a un archivo csv", "mute_export": "Exportar silenciados", - "hide_wallpaper": "Ocultar el fondo de pantalla de la instancia" + "hide_wallpaper": "Ocultar el fondo de pantalla de la instancia", + "setting_changed": "La configuración es diferente a la predeterminada" }, "time": { "day": "{0} día", @@ -693,7 +694,11 @@ "show_repeats": "Mostrar repetidos", "hide_repeats": "Ocultar repetidos", "message": "Mensaje", - "hidden": "Oculto" + "hidden": "Oculto", + "roles": { + "moderator": "Moderador", + "admin": "Administrador" + } }, "user_profile": { "timeline_title": "Linea Temporal del Usuario", diff --git a/src/i18n/fr.json b/src/i18n/fr.json @@ -280,7 +280,7 @@ "hide_followers_description": "Ne pas afficher qui est abonné à moi", "show_admin_badge": "Afficher le badge d'Administrateur⋅ice sur mon profil", "show_moderator_badge": "Afficher le badge de Modérateur⋅ice sur mon profil", - "nsfw_clickthrough": "Masquer les images marquées comme contenu adulte ou sensible", + "nsfw_clickthrough": "Activer le clic pour dévoiler les pièces jointes et cacher l'aperçu des liens pour les statuts marqués comme sensibles", "oauth_tokens": "Jetons OAuth", "token": "Jeton", "refresh_token": "Rafraichir le jeton", @@ -409,7 +409,13 @@ "tabs": "Onglets", "toggled": "(Dés)activé", "highlight": "Éléments mis en valeur", - "popover": "Infobulles, menus" + "popover": "Infobulles, menus", + "chat": { + "border": "Bordure", + "outgoing": "Sortant(s)", + "incoming": "Entrant(s)" + }, + "wallpaper": "Fond d'écran" }, "radii": { "_tab_label": "Rondeur" @@ -485,7 +491,7 @@ "notification_visibility_emoji_reactions": "Réactions", "hide_follows_count_description": "Masquer le nombre de suivis", "useStreamingApiWarning": "(Non recommandé, expérimental, connu pour rater des messages)", - "type_domains_to_mute": "Écrire les domaines à masquer", + "type_domains_to_mute": "Chercher les domaines à masquer", "fun": "Rigolo", "greentext": "greentexting", "allow_following_move": "Suivre automatiquement quand ce compte migre", @@ -509,7 +515,21 @@ "mute_import_error": "Erreur à l'import des masquages", "mute_import": "Import des masquages", "mute_export_button": "Exporter vos masquages dans un fichier CSV", - "mute_export": "Export des masquages" + "mute_export": "Export des masquages", + "notification_setting_hide_notification_contents": "Cacher l'expéditeur et le contenu des notifications push", + "notification_setting_block_from_strangers": "Bloquer les notifications des utilisateur⋅ice⋅s que vous ne suivez pas", + "virtual_scrolling": "Optimiser le rendu du fil d'actualité", + "reset_background_confirm": "Voulez-vraiment réinitialiser l'arrière-plan ?", + "reset_banner_confirm": "Voulez-vraiment réinitialiser la bannière ?", + "reset_avatar_confirm": "Voulez-vraiment réinitialiser l'avatar ?", + "reset_profile_banner": "Réinitialiser la bannière du profil", + "reset_profile_background": "Réinitialiser l'arrière-plan du profil", + "reset_avatar": "Réinitialiser l'avatar", + "profile_fields": { + "value": "Contenu", + "name": "Étiquette", + "add_field": "Ajouter un champ" + } }, "timeline": { "collapse": "Fermer", @@ -521,7 +541,9 @@ "show_new": "Afficher plus", "up_to_date": "À jour", "no_more_statuses": "Pas plus de statuts", - "no_statuses": "Aucun statuts" + "no_statuses": "Aucun statuts", + "reload": "Recharger", + "error": "Erreur lors de l'affichage du fil d'actualité : {0}" }, "status": { "favorites": "Favoris", @@ -536,7 +558,19 @@ "mute_conversation": "Masquer la conversation", "unmute_conversation": "Démasquer la conversation", "status_unavailable": "Status indisponible", - "copy_link": "Copier le lien au status" + "copy_link": "Copier le lien au status", + "expand": "Développer", + "nsfw": "Contenu sensible", + "status_deleted": "Ce post a été effacé", + "hide_content": "Cacher le contenu", + "show_content": "Montrer le contenu", + "hide_full_subject": "Cacher le sujet", + "show_full_subject": "Montrer le sujet en entier", + "thread_muted_and_words": ", contient les mots :", + "thread_muted": "Fil de discussion masqué", + "external_source": "Source externe", + "unbookmark": "Supprimer des favoris", + "bookmark": "Ajouter aux favoris" }, "user_card": { "approve": "Accepter", @@ -591,7 +625,12 @@ "subscribe": "Abonner", "unsubscribe": "Désabonner", "hide_repeats": "Cacher les partages", - "show_repeats": "Montrer les partages" + "show_repeats": "Montrer les partages", + "roles": { + "moderator": "Modérateur⋅ice", + "admin": "Administrateur⋅ice" + }, + "message": "Message" }, "user_profile": { "timeline_title": "Journal de l'utilisateur⋅ice", @@ -619,13 +658,15 @@ "user_settings": "Paramètres utilisateur", "add_reaction": "Ajouter une réaction", "accept_follow_request": "Accepter la demande de suivit", - "reject_follow_request": "Rejeter la demande de suivit" + "reject_follow_request": "Rejeter la demande de suivit", + "bookmark": "Favori" }, "upload": { "error": { "base": "L'envoi a échoué.", "file_too_big": "Fichier trop gros [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Réessayez plus tard" + "default": "Réessayez plus tard", + "message": "Envoi échoué : {0}" }, "file_size_units": { "B": "O", @@ -759,5 +800,27 @@ }, "shoutbox": { "title": "Shoutbox" + }, + "display_date": { + "today": "Aujourd'hui" + }, + "file_type": { + "file": "Fichier", + "image": "Image", + "video": "Vidéo", + "audio": "Audio" + }, + "chats": { + "empty_chat_list_placeholder": "Vous n'avez pas encore de discussions. Démarrez-en une nouvelle !", + "error_sending_message": "Quelque chose s'est mal passé pendant l'envoi du message.", + "error_loading_chat": "Quelque chose s'est mal passé au chargement de la discussion.", + "delete_confirm": "Voulez-vous vraiment effacer ce message ?", + "more": "Plus", + "empty_message_error": "Impossible d'envoyer un message vide", + "new": "Nouvelle discussion", + "chats": "Discussions", + "delete": "Effacer", + "message_user": "Message à {nickname}", + "you": "Vous :" } } diff --git a/src/i18n/it.json b/src/i18n/it.json @@ -17,7 +17,11 @@ "close": "Chiudi", "retry": "Riprova", "error_retry": "Per favore, riprova", - "loading": "Carico…" + "loading": "Carico…", + "role": { + "moderator": "Moderatore", + "admin": "Amministratore" + } }, "nav": { "mentions": "Menzioni", @@ -30,7 +34,7 @@ "administration": "Amministrazione", "back": "Indietro", "interactions": "Interazioni", - "dms": "Messaggi diretti", + "dms": "Messaggi privati", "user_search": "Ricerca utenti", "search": "Ricerca", "who_to_follow": "Chi seguire", @@ -44,7 +48,7 @@ "notifications": "Notifiche", "read": "Letto!", "broken_favorite": "Stato sconosciuto, lo sto cercando…", - "favorited_you": "ha gradito il tuo messaggio", + "favorited_you": "gradisce il tuo messaggio", "load_older": "Carica notifiche precedenti", "repeated_you": "ha condiviso il tuo messaggio", "follow_request": "vuole seguirti", @@ -417,7 +421,8 @@ "mute_import": "Importa silenziati", "mute_export_button": "Esporta la tua lista di silenziati in un file CSV", "mute_export": "Esporta silenziati", - "hide_wallpaper": "Nascondi sfondo della stanza" + "hide_wallpaper": "Nascondi sfondo della stanza", + "setting_changed": "Valore personalizzato" }, "timeline": { "error_fetching": "Errore nell'aggiornamento", @@ -487,7 +492,8 @@ "follow_progress": "Richiedo…", "follow_sent": "Richiesta inviata!", "favorites": "Preferiti", - "message": "Contatta" + "message": "Contatta", + "bot": "Bot" }, "chat": { "title": "Chat" @@ -515,18 +521,18 @@ "register": "Registrati", "username": "Nome utente", "description": "Accedi con OAuth", - "hint": "Accedi per partecipare alla discussione", + "hint": "Accedi per conversare", "authentication_code": "Codice di autenticazione", "enter_recovery_code": "Inserisci un codice di recupero", - "enter_two_factor_code": "Inserisci un codice two-factor", + "enter_two_factor_code": "Inserisci un codice 2FA", "recovery_code": "Codice di recupero", "heading": { - "totp": "Autenticazione two-factor", - "recovery": "Recupero two-factor" + "totp": "Autenticazione 2FA", + "recovery": "Recupero 2FA" } }, "post_status": { - "account_not_locked_warning": "Il tuo profilo non è {0}. Chiunque può seguirti e vedere i tuoi messaggi riservati ai tuoi seguaci.", + "account_not_locked_warning": "Il tuo profilo non è {0}. Chiunque può seguirti e vedere i tuoi messaggi per seguaci.", "account_not_locked_warning_link": "protetto", "attachments_sensitive": "Nascondi gli allegati", "content_type": { @@ -536,7 +542,7 @@ "text/html": "HTML" }, "content_warning": "Oggetto (facoltativo)", - "default": "Sono appena atterrato a Fiumicino.", + "default": "Sono appena atterrato a Città Laggiù.", "direct_warning": "Questo post sarà visibile solo dagli utenti menzionati.", "posting": "Sto pubblicando", "scope": { @@ -578,7 +584,9 @@ "fullname_placeholder": "es. Lupo Lucio", "username_placeholder": "es. mister_wolf", "new_captcha": "Clicca l'immagine per avere un altro captcha", - "captcha": "CAPTCHA" + "captcha": "CAPTCHA", + "reason_placeholder": "L'amministratore esamina ciascuna richiesta.\nFornisci il motivo della tua iscrizione.", + "reason": "Motivo dell'iscrizione" }, "user_profile": { "timeline_title": "Sequenza dell'Utente", @@ -646,20 +654,22 @@ }, "polls": { "add_poll": "Sondaggio", - "add_option": "Alternativa", + "add_option": "Aggiungi opzione", "option": "Opzione", "votes": "voti", "vote": "Vota", "type": "Tipo di sondaggio", "single_choice": "Scelta singola", "multiple_choices": "Scelta multipla", - "expiry": "Scadenza", - "expires_in": "Scade fra {0}", - "expired": "Scaduto {0} fa", - "not_enough_options": "Aggiungi altre risposte" + "expiry": "Età", + "expires_in": "Chiude fra {0}", + "expired": "Chiuso {0} fa", + "not_enough_options": "Aggiungi altre risposte", + "votes_count": "{count} voto | {count} voti", + "people_voted_count": "{count} votante | {count} votanti" }, "interactions": { - "favs_repeats": "Condivisi e preferiti", + "favs_repeats": "Condivisi e Graditi", "load_older": "Carica vecchie interazioni", "moves": "Utenti migrati", "follows": "Nuovi seguìti" @@ -668,8 +678,8 @@ "load_all": "Carico tutti i {emojiAmount} emoji", "load_all_hint": "Primi {saneAmount} emoji caricati, caricarli tutti potrebbe causare rallentamenti.", "unicode": "Emoji Unicode", - "custom": "Emoji personale", - "add_emoji": "Inserisci Emoji", + "custom": "Emoji della stanza", + "add_emoji": "Inserisci emoji", "search_emoji": "Cerca un emoji", "keep_open": "Tieni aperto il menù", "emoji": "Emoji", @@ -684,7 +694,7 @@ "remote_user_resolver": "Cerca utenti remoti" }, "errors": { - "storage_unavailable": "Pleroma non ha potuto accedere ai dati del tuo browser. Le tue credenziali o le tue impostazioni locali non potranno essere salvate e potresti incontrare strani errori. Prova ad abilitare i cookie." + "storage_unavailable": "Pleroma non può accedere ai dati del tuo browser. Il tuo accesso o le tue impostazioni non saranno salvate e potresti incontrare strani errori. Prova ad abilitare i cookie." }, "status": { "pinned": "Intestato", diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json @@ -4,7 +4,7 @@ }, "exporter": { "export": "エクスポート", - "processing": "処理中です。処理が完了すると、ファイルをダウンロードするよう指示があります。" + "processing": "処理中です。処理が完了すると、ファイルをダウンロードするよう指示があります" }, "features_panel": { "chat": "チャット", @@ -13,10 +13,12 @@ "scope_options": "公開範囲選択", "text_limit": "文字の数", "title": "有効な機能", - "who_to_follow": "おすすめユーザー" + "who_to_follow": "おすすめユーザー", + "upload_limit": "ファイルサイズの上限", + "pleroma_chat_messages": "Pleroma チャット" }, "finder": { - "error_fetching_user": "ユーザー検索がエラーになりました。", + "error_fetching_user": "ユーザー検索がエラーになりました", "find_user": "ユーザーを探す" }, "general": { @@ -31,7 +33,17 @@ "disable": "無効", "enable": "有効", "confirm": "確認", - "verify": "検査" + "verify": "検査", + "peek": "隠す", + "close": "閉じる", + "dismiss": "無視", + "retry": "もう一度お試し下さい", + "error_retry": "もう一度お試し下さい", + "loading": "読み込み中…", + "role": { + "moderator": "モデレーター", + "admin": "管理者" + } }, "image_cropper": { "crop_picture": "画像を切り抜く", @@ -57,9 +69,9 @@ "enter_recovery_code": "リカバリーコードを入力してください", "enter_two_factor_code": "2段階認証コードを入力してください", "recovery_code": "リカバリーコード", - "heading" : { - "totp" : "2段階認証", - "recovery" : "2段階リカバリー" + "heading": { + "totp": "2段階認証", + "recovery": "2段階リカバリー" } }, "media_modal": { @@ -76,21 +88,29 @@ "dms": "ダイレクトメッセージ", "public_tl": "パブリックタイムライン", "timeline": "タイムライン", - "twkn": "接続しているすべてのネットワーク", + "twkn": "すべてのネットワーク", "user_search": "ユーザーを探す", "search": "検索", "who_to_follow": "おすすめユーザー", - "preferences": "設定" + "preferences": "設定", + "administration": "管理", + "bookmarks": "ブックマーク", + "timelines": "タイムライン", + "chats": "チャット" }, "notifications": { - "broken_favorite": "ステータスが見つかりません。探しています...", + "broken_favorite": "ステータスが見つかりません。探しています…", "favorited_you": "あなたのステータスがお気に入りされました", "followed_you": "フォローされました", "load_older": "古い通知をみる", "notifications": "通知", "read": "読んだ!", "repeated_you": "あなたのステータスがリピートされました", - "no_more_notifications": "通知はありません" + "no_more_notifications": "通知はありません", + "reacted_with": "{0} でリアクションしました", + "migrated_to": "インスタンスを引っ越しました", + "follow_request": "あなたをフォローしたいです", + "error": "通知の取得に失敗しました: {0}" }, "polls": { "add_poll": "投票を追加", @@ -104,7 +124,9 @@ "expiry": "投票期間", "expires_in": "投票は {0} で終了します", "expired": "投票は {0} 前に終了しました", - "not_enough_options": "相異なる選択肢が不足しています" + "not_enough_options": "相異なる選択肢が不足しています", + "votes_count": "{count} 票 | {count} 票", + "people_voted_count": "{count} 人投票 | {count} 人投票" }, "emoji": { "stickers": "ステッカー", @@ -113,7 +135,9 @@ "search_emoji": "絵文字を検索", "add_emoji": "絵文字を挿入", "custom": "カスタム絵文字", - "unicode": "Unicode絵文字" + "unicode": "Unicode絵文字", + "load_all": "全 {emojiAmount} 絵文字を読み込む", + "load_all_hint": "最初の {saneAmount} 絵文字を読み込みました、全て読み込むと重くなる可能性があります。" }, "stickers": { "add_sticker": "ステッカーを追加" @@ -121,7 +145,8 @@ "interactions": { "favs_repeats": "リピートとお気に入り", "follows": "新しいフォロワー", - "load_older": "古いインタラクションを見る" + "load_older": "古いインタラクションを見る", + "moves": "ユーザーの引っ越し" }, "post_status": { "new_status": "投稿する", @@ -142,15 +167,20 @@ "posting": "投稿", "scope_notice": { "public": "この投稿は、誰でも見ることができます", - "private": "この投稿は、あなたのフォロワーだけが、見ることができます。", - "unlisted": "この投稿は、パブリックタイムラインと、接続しているすべてのネットワークには、表示されません。" + "private": "この投稿は、あなたのフォロワーだけが、見ることができます", + "unlisted": "この投稿は、パブリックタイムラインと、接続しているすべてのネットワークには、表示されません" }, "scope": { - "direct": "ダイレクト: メンションされたユーザーのみに届きます。", - "private": "フォロワーげんてい: フォロワーのみに届きます。", - "public": "パブリック: パブリックタイムラインに届きます。", - "unlisted": "アンリステッド: パブリックタイムラインに届きません。" - } + "direct": "ダイレクト: メンションされたユーザーのみに届きます", + "private": "フォロワー限定: フォロワーのみに届きます", + "public": "パブリック: パブリックタイムラインに届きます", + "unlisted": "アンリステッド: パブリックタイムラインに届きません" + }, + "media_description_error": "メディアのアップロードに失敗しました。もう一度お試しください", + "empty_status_error": "投稿内容を入力してください", + "preview_empty": "何もありません", + "preview": "プレビュー", + "media_description": "メディアの説明" }, "registration": { "bio": "プロフィール", @@ -171,7 +201,9 @@ "password_required": "必須", "password_confirmation_required": "必須", "password_confirmation_match": "パスワードが違います" - } + }, + "reason_placeholder": "このインスタンスは、新規登録を手動で受け付けています。\n登録したい理由を、インスタンスの管理者に教えてください。", + "reason": "登録するための目的" }, "selectable_list": { "select_all": "すべて選択" @@ -181,17 +213,17 @@ "security": "セキュリティ", "enter_current_password_to_confirm": "あなたのアイデンティティを証明するため、現在のパスワードを入力してください", "mfa": { - "otp" : "OTP", - "setup_otp" : "OTPのセットアップ", - "wait_pre_setup_otp" : "OTPのプリセット", - "confirm_and_enable" : "OTPの確認と有効化", + "otp": "OTP", + "setup_otp": "OTPのセットアップ", + "wait_pre_setup_otp": "OTPのプリセット", + "confirm_and_enable": "OTPの確認と有効化", "title": "2段階認証", - "generate_new_recovery_codes" : "新しいリカバリーコードを生成", - "warning_of_generate_new_codes" : "新しいリカバリーコードを生成すると、古いコードは使用できなくなります。", - "recovery_codes" : "リカバリーコード。", - "waiting_a_recovery_codes": "バックアップコードを受信しています...", - "recovery_codes_warning" : "コードを紙に書くか、安全な場所に保存してください。そうでなければ、あなたはコードを再び見ることはできません。もし2段階認証アプリのアクセスを喪失し、なおかつ、リカバリーコードもないならば、あなたは自分のアカウントから閉め出されます。", - "authentication_methods" : "認証方法", + "generate_new_recovery_codes": "新しいリカバリーコードを生成", + "warning_of_generate_new_codes": "新しいリカバリーコードを生成すると、古いコードは使用できなくなります。", + "recovery_codes": "リカバリーコード。", + "waiting_a_recovery_codes": "バックアップコードを受信しています…", + "recovery_codes_warning": "コードを紙に書くか、安全な場所に保存してください。そうでなければ、あなたはコードを再び見ることはできません。もし2段階認証アプリのアクセスを喪失し、なおかつ、リカバリーコードもないならば、あなたは自分のアカウントから閉め出されます。", + "authentication_methods": "認証方法", "scan": { "title": "スキャン", "desc": "あなたの2段階認証アプリを使って、このQRコードをスキャンするか、テキストキーを入力してください:", @@ -231,7 +263,7 @@ "data_import_export_tab": "インポートとエクスポート", "default_vis": "デフォルトの公開範囲", "delete_account": "アカウントを消す", - "delete_account_description": "あなたのアカウントとメッセージが、消えます。", + "delete_account_description": "あなたのデータが消えて、アカウントが使えなくなります。", "delete_account_error": "アカウントを消すことが、できなかったかもしれません。インスタンスの管理者に、連絡してください。", "delete_account_instructions": "本当にアカウントを消してもいいなら、パスワードを入力してください。", "discoverable": "検索などのサービスでこのアカウントを見つけることを許可する", @@ -239,12 +271,12 @@ "pad_emoji": "ピッカーから絵文字を挿入するとき、絵文字の両側にスペースを入れる", "export_theme": "保存", "filtering": "フィルタリング", - "filtering_explanation": "これらの言葉を含むすべてのものがミュートされます。1行に1つの言葉を書いてください。", + "filtering_explanation": "これらの言葉を含むすべてのものがミュートされます。1行に1つの言葉を書いてください", "follow_export": "フォローのエクスポート", "follow_export_button": "エクスポート", "follow_export_processing": "お待ちください。まもなくファイルをダウンロードできます。", "follow_import": "フォローのインポート", - "follow_import_error": "フォローのインポートがエラーになりました。", + "follow_import_error": "フォローのインポートがエラーになりました", "follows_imported": "フォローがインポートされました! 少し時間がかかるかもしれません。", "foreground": "フォアグラウンド", "general": "全般", @@ -305,7 +337,7 @@ "profile_background": "プロフィールのバックグラウンド", "profile_banner": "プロフィールバナー", "profile_tab": "プロフィール", - "radii_help": "インターフェースの丸さを設定する。", + "radii_help": "インターフェースの丸さを設定する", "replies_in_timeline": "タイムラインのリプライ", "reply_visibility_all": "すべてのリプライを見る", "reply_visibility_following": "私に宛てられたリプライと、フォローしている人からのリプライを見る", @@ -332,7 +364,7 @@ "streaming": "上までスクロールしたとき、自動的にストリーミングする", "text": "文字", "theme": "テーマ", - "theme_help": "カラーテーマをカスタマイズできます", + "theme_help": "カラーテーマをカスタマイズできます。", "theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、色と透明度をオーバーライドできます。「すべてクリア」ボタンを押すと、すべてのオーバーライドをやめます。", "theme_help_v2_2": "バックグラウンドとテキストのコントラストを表すアイコンがあります。マウスをホバーすると、詳しい説明が出ます。透明な色を使っているときは、最悪の場合のコントラストが示されます。", "tooltipRadius": "ツールチップとアラート", @@ -356,7 +388,24 @@ "save_load_hint": "「残す」オプションをONにすると、テーマを選んだときとロードしたとき、現在の設定を残します。また、テーマをエクスポートするとき、これらのオプションを維持します。すべてのチェックボックスをOFFにすると、テーマをエクスポートしたとき、すべての設定を保存します。", "reset": "リセット", "clear_all": "すべてクリア", - "clear_opacity": "透明度をクリア" + "clear_opacity": "透明度をクリア", + "help": { + "snapshot_missing": "テーマのスナップショットがありません。思っていた見た目と違うかもしれません。", + "migration_snapshot_ok": "念のために、テーマのスナップショットが読み込まれました。テーマのデータを読み込むことができます。", + "fe_downgraded": "フロントエンドが前のバージョンに戻りました。", + "fe_upgraded": "フロントエンドと一緒に、テーマエンジンが新しくなりました。", + "older_version_imported": "古いフロントエンドで作られたファイルをインポートしました。", + "future_version_imported": "新しいフロントエンドで作られたファイルをインポートしました。", + "v2_imported": "古いフロントエンドのためのファイルをインポートしました。設定した通りにならないかもしれません。", + "upgraded_from_v2": "フロントエンドが新しくなったので、今までの見た目と少し違うかもしれません。", + "snapshot_source_mismatch": "フロントエンドがロールバックと更新を繰り返したため、バージョンが競合しています。", + "migration_napshot_gone": "スナップショットがありません、覚えているものと見た目が違うかもしれません。", + "snapshot_present": "テーマのスナップショットが読み込まれました。設定は上書きされました。代わりとして実データを読み込むことができます。" + }, + "use_source": "新しいバージョン", + "use_snapshot": "古いバージョン", + "load_theme": "テーマの読み込み", + "keep_as_is": "変更しない" }, "common": { "color": "色", @@ -364,9 +413,9 @@ "contrast": { "hint": "コントラストは {ratio} です。{level}。({context})", "level": { - "aa": "AAレベルガイドライン (ミニマル) を満たします", - "aaa": "AAAレベルガイドライン (レコメンデッド) を満たします。", - "bad": "ガイドラインを満たしません。" + "aa": "AAレベルガイドライン (最低限) を満たします", + "aaa": "AAAレベルガイドライン (推奨) を満たします", + "bad": "ガイドラインを満たしません" }, "context": { "18pt": "大きい (18ポイント以上) テキスト", @@ -391,7 +440,27 @@ "borders": "境界", "buttons": "ボタン", "inputs": "インプットフィールド", - "faint_text": "薄いテキスト" + "faint_text": "薄いテキスト", + "alert_neutral": "それ以外", + "chat": { + "border": "境界線", + "outgoing": "送信", + "incoming": "受信" + }, + "tabs": "タブ", + "toggled": "切り替えたとき", + "disabled": "無効なとき", + "selectedMenu": "選択されたメニューアイテム", + "selectedPost": "選択された投稿", + "pressed": "押したとき", + "highlight": "強調された要素", + "icons": "アイコン", + "poll": "投票グラフ", + "wallpaper": "壁紙", + "underlay": "アンダーレイ", + "popover": "ツールチップ、メニュー、ポップオーバー", + "post": "投稿/プロフィール", + "alert_warning": "警告" }, "radii": { "_tab_label": "丸さ" @@ -409,8 +478,8 @@ "always_drop_shadow": "ブラウザーがサポートしていれば、常に {0} が使われます。", "drop_shadow_syntax": "{0} は、{1} パラメーターと {2} キーワードをサポートしていません。", "avatar_inset": "内側の影と外側の影を同時に使うと、透明なアバターの表示が乱れます。", - "spread_zero": "広がりが 0 よりも大きな影は、0 と同じです。", - "inset_classic": "内側の影は {0} を使います。" + "spread_zero": "広がりが 0 よりも大きな影は、0 と同じです", + "inset_classic": "内側の影は {0} を使います" }, "components": { "panel": "パネル", @@ -424,7 +493,8 @@ "buttonPressed": "ボタン (押されているとき)", "buttonPressedHover": "ボタン (ホバー、かつ、押されているとき)", "input": "インプットフィールド" - } + }, + "hintV3": "影の場合は、 {0} 表記を使って他の色スロットを使うこともできます。" }, "fonts": { "_tab_label": "フォント", @@ -445,7 +515,7 @@ "content": "本文", "error": "エラーの例", "button": "ボタン", - "text": "これは{0}と{1}の例です。", + "text": "これは{0}と{1}の例です", "mono": "monospace", "input": "羽田空港に着きました。", "faint_link": "とても助けになるマニュアル", @@ -459,7 +529,52 @@ "title": "バージョン", "backend_version": "バックエンドのバージョン", "frontend_version": "フロントエンドのバージョン" - } + }, + "notification_setting_hide_notification_contents": "送った人と内容を、プッシュ通知に表示しない", + "notification_setting_privacy": "プライバシー", + "notification_setting_block_from_strangers": "フォローしていないユーザーからの通知を拒否する", + "notification_setting_filters": "フィルター", + "fun": "お楽しみ", + "virtual_scrolling": "タイムラインの描画を最適化する", + "type_domains_to_mute": "ミュートしたいドメインを検索", + "useStreamingApiWarning": "(実験中で、投稿を取りこぼすかもしれないので、おすすめしません)", + "useStreamingApi": "投稿と通知を、すぐに受け取る", + "user_mutes": "ユーザー", + "reset_background_confirm": "本当にバックグラウンドを初期化しますか?", + "reset_banner_confirm": "本当にバナーを初期化しますか?", + "reset_avatar_confirm": "本当にアバターを初期化しますか?", + "hide_wallpaper": "インスタンスのバックグラウンドを隠す", + "reset_profile_background": "プロフィールのバックグラウンドを初期化", + "reset_profile_banner": "プロフィールのバナーを初期化", + "reset_avatar": "アバターを初期化", + "notification_visibility_emoji_reactions": "リアクション", + "notification_visibility_moves": "ユーザーの引っ越し", + "new_email": "新しいメールアドレス", + "profile_fields": { + "value": "内容", + "name": "ラベル", + "add_field": "枠を追加", + "label": "プロフィール補足情報" + }, + "accent": "アクセント", + "mutes_imported": "ミュートをインポートしました!少し時間がかかるかもしれません。", + "emoji_reactions_on_timeline": "絵文字リアクションをタイムラインに表示", + "domain_mutes": "ドメイン", + "mutes_and_blocks": "ミュートとブロック", + "chatMessageRadius": "チャットメッセージ", + "change_email_error": "メールアドレスを変えることが、できなかったかもしれません。", + "changed_email": "メールアドレスが、変わりました!", + "change_email": "メールアドレスを変える", + "bot": "これは bot アカウントです", + "mute_export_button": "ミュートをCSVファイルにエクスポートする", + "import_mutes_from_a_csv_file": "CSVファイルからミュートをインポートする", + "mute_import_error": "ミュートのインポートに失敗しました", + "mute_import": "ミュートのインポート", + "mute_export": "ミュートのエクスポート", + "allow_following_move": "フォロー中のアカウントが引っ越したとき、自動フォローを許可する", + "setting_changed": "規定の設定と異なっています", + "greentext": "引用を緑色で表示", + "sensitive_by_default": "はじめから投稿をセンシティブとして設定" }, "time": { "day": "{0}日", @@ -505,7 +620,9 @@ "show_new": "読み込み", "up_to_date": "最新", "no_more_statuses": "これで終わりです", - "no_statuses": "ステータスはありません" + "no_statuses": "ステータスはありません", + "reload": "再読み込み", + "error": "タイムラインの読み込みに失敗しました: {0}" }, "status": { "favorites": "お気に入り", @@ -518,7 +635,21 @@ "reply_to": "返信", "replies_list": "返信:", "mute_conversation": "スレッドをミュート", - "unmute_conversation": "スレッドのミュートを解除" + "unmute_conversation": "スレッドのミュートを解除", + "nsfw": "閲覧注意", + "expand": "広げる", + "status_deleted": "この投稿は削除されました", + "hide_content": "隠す", + "show_content": "見る", + "hide_full_subject": "隠す", + "show_full_subject": "全部見る", + "thread_muted_and_words": "以下の単語を含むため:", + "thread_muted": "ミュートされたスレッド", + "external_source": "外部ソース", + "copy_link": "リンクをコピー", + "status_unavailable": "利用できません", + "unbookmark": "ブックマーク解除", + "bookmark": "ブックマーク" }, "user_card": { "approve": "受け入れ", @@ -539,7 +670,7 @@ "media": "メディア", "mention": "メンション", "mute": "ミュート", - "muted": "ミュートしています!", + "muted": "ミュートしています", "per_day": "/日", "remote_follow": "リモートフォロー", "report": "通報", @@ -547,11 +678,11 @@ "subscribe": "購読", "unsubscribe": "購読を解除", "unblock": "ブロック解除", - "unblock_progress": "ブロックを解除しています...", - "block_progress": "ブロックしています...", + "unblock_progress": "ブロックを解除しています…", + "block_progress": "ブロックしています…", "unmute": "ミュート解除", - "unmute_progress": "ミュートを解除しています...", - "mute_progress": "ミュートしています...", + "unmute_progress": "ミュートを解除しています…", + "mute_progress": "ミュートしています…", "admin_menu": { "moderation": "モデレーション", "grant_admin": "管理者権限を付与", @@ -570,7 +701,16 @@ "quarantine": "他のインスタンスからの投稿を止める", "delete_user": "ユーザーを削除", "delete_user_confirmation": "あなたの精神状態に何か問題はございませんか? この操作を取り消すことはできません。" - } + }, + "roles": { + "moderator": "モデレーター", + "admin": "管理者" + }, + "show_repeats": "リピートを見る", + "hide_repeats": "リピートを隠す", + "message": "メッセージ", + "hidden": "隠す", + "bot": "bot" }, "user_profile": { "timeline_title": "ユーザータイムライン", @@ -595,13 +735,18 @@ "repeat": "リピート", "reply": "返信", "favorite": "お気に入り", - "user_settings": "ユーザー設定" + "user_settings": "ユーザー設定", + "bookmark": "ブックマーク", + "reject_follow_request": "フォローリクエストを拒否", + "accept_follow_request": "フォローリクエストを許可", + "add_reaction": "リアクションを追加" }, - "upload":{ + "upload": { "error": { - "base": "アップロードに失敗しました。", - "file_too_big": "ファイルが大きすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", - "default": "しばらくしてから試してください" + "base": "アップロードに失敗しました。", + "file_too_big": "ファイルが大きすぎます [{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", + "default": "しばらくしてから試してください", + "message": "アップロードに失敗: {0}" }, "file_size_units": { "B": "B", @@ -626,6 +771,77 @@ "check_email": "パスワードをリセットするためのリンクが記載されたメールが届いているか確認してください。", "return_home": "ホームページに戻る", "too_many_requests": "試行回数の制限に達しました。しばらく時間を置いてから再試行してください。", - "password_reset_disabled": "このインスタンスではパスワードリセットは無効になっています。インスタンスの管理者に連絡してください。" + "password_reset_disabled": "このインスタンスではパスワードリセットは無効になっています。インスタンスの管理者に連絡してください。", + "password_reset_required_but_mailer_is_disabled": "パスワードの初期化が必要ですが、初期化は使えません。インスタンスの管理者に連絡してください。", + "password_reset_required": "ログインするためにパスワードを初期化してください。" + }, + "about": { + "mrf": { + "mrf_policies_desc": "MRFポリシーは、インスタンスの振る舞いを操作します。以下のポリシーが有効になっています:", + "federation": "連合", + "simple": { + "media_nsfw_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを閲覧注意に設定します:", + "media_nsfw": "メディアを閲覧注意に設定", + "media_removal_desc": "このインスタンスでは、以下のインスタンスからの投稿に対して、メディアを除去します:", + "media_removal": "メディア除去", + "ftl_removal": "「接続しているすべてのネットワーク」タイムラインから除外", + "ftl_removal_desc": "このインスタンスでは、以下のインスタンスを「接続しているすべてのネットワーク」タイムラインから除外します:", + "quarantine_desc": "このインスタンスでは、以下のインスタンスに対して公開投稿のみを送信します:", + "quarantine": "検疫", + "reject_desc": "このインスタンスでは、以下のインスタンスからのメッセージを受け付けません:", + "accept_desc": "このインスタンスでは、以下のインスタンスからのメッセージのみを受け付けます:", + "accept": "許可", + "simple_policies": "インスタンス固有のポリシー", + "reject": "拒否" + }, + "mrf_policies": "有効なMRFポリシー", + "keyword": { + "replace": "置き換え", + "ftl_removal": "「接続しているすべてのネットワーク」タイムラインから除外", + "keyword_policies": "キーワードポリシー", + "is_replaced_by": "→", + "reject": "拒否" + } + }, + "staff": "スタッフ" + }, + "display_date": { + "today": "今日" + }, + "file_type": { + "file": "ファイル", + "image": "画像", + "video": "ビデオ", + "audio": "オーディオ" + }, + "remote_user_resolver": { + "error": "見つかりませんでした。", + "searching_for": "検索中", + "remote_user_resolver": "リモートユーザーリゾルバ" + }, + "errors": { + "storage_unavailable": "ブラウザのストレージに接続できなかったため、ログインや設定情報は保存されません。Cookieを有効にしてください。" + }, + "shoutbox": { + "title": "Shoutbox" + }, + "chats": { + "empty_chat_list_placeholder": "チャットはありません。新規チャットのボタンを押して始めましょう!", + "error_sending_message": "メッセージの送信に失敗しました。", + "error_loading_chat": "チャットの読み込みに失敗しました。", + "delete_confirm": "このメッセージを本当に消してもいいですか?", + "more": "もっと見る", + "empty_message_error": "メッセージを入力して下さい", + "new": "新規チャット", + "chats": "チャット一覧", + "delete": "削除", + "message_user": "{nickname} にメッセージ", + "you": "あなた:" + }, + "domain_mute_card": { + "unmute_progress": "ミュート解除中…", + "unmute": "ミュート解除", + "mute_progress": "ミュート中…", + "mute": "ミュート" } } diff --git a/src/i18n/ko.json b/src/i18n/ko.json @@ -9,7 +9,9 @@ "scope_options": "범위 옵션", "text_limit": "텍스트 제한", "title": "기능", - "who_to_follow": "팔로우 추천" + "who_to_follow": "팔로우 추천", + "upload_limit": "최대 파일용량", + "pleroma_chat_messages": "Pleroma 채트" }, "finder": { "error_fetching_user": "사용자 정보 불러오기 실패", @@ -17,7 +19,27 @@ }, "general": { "apply": "적용", - "submit": "보내기" + "submit": "보내기", + "loading": "로딩중…", + "peek": "숨기기", + "close": "닫기", + "verify": "검사", + "confirm": "확인", + "enable": "유효", + "disable": "무효", + "cancel": "취소", + "dismiss": "무시", + "show_less": "접기", + "show_more": "더 보기", + "optional": "필수 아님", + "retry": "다시 시도하십시오", + "error_retry": "다시 시도하십시오", + "generic_error": "잘못되었습니다", + "more": "더 보기", + "role": { + "moderator": "중재자", + "admin": "관리자" + } }, "login": { "login": "로그인", @@ -26,10 +48,19 @@ "password": "암호", "placeholder": "예시: lain", "register": "가입", - "username": "사용자 이름" + "username": "사용자 이름", + "heading": { + "recovery": "2단계 복구", + "totp": "2단계인증" + }, + "recovery_code": "복구 코드", + "enter_two_factor_code": "2단계인증 코드를 입력하십시오", + "enter_recovery_code": "복구 코드를 입력하십시오", + "authentication_code": "인증 코드", + "hint": "로그인하여 대화에 참가합시다" }, "nav": { - "about": "About", + "about": "인스턴스 소개", "back": "뒤로", "chat": "로컬 챗", "friend_requests": "팔로우 요청", @@ -37,18 +68,29 @@ "dms": "다이렉트 메시지", "public_tl": "공개 타임라인", "timeline": "타임라인", - "twkn": "모든 알려진 네트워크", + "twkn": "알려진 네트워크", "user_search": "사용자 검색", - "preferences": "환경설정" + "preferences": "환경설정", + "chats": "채트", + "timelines": "타임라인", + "who_to_follow": "추천된 사용자", + "search": "검색", + "bookmarks": "북마크", + "interactions": "대화", + "administration": "관리" }, "notifications": { - "broken_favorite": "알 수 없는 게시물입니다, 검색 합니다...", + "broken_favorite": "알 수 없는 게시물입니다, 검색합니다…", "favorited_you": "당신의 게시물을 즐겨찾기", "followed_you": "당신을 팔로우", "load_older": "오래 된 알림 불러오기", "notifications": "알림", "read": "읽음!", - "repeated_you": "당신의 게시물을 리핏" + "repeated_you": "당신의 게시물을 리핏", + "no_more_notifications": "알림이 없습니다", + "migrated_to": "이사했습니다", + "reacted_with": "{0} 로 반응했습니다", + "error": "알림 불러오기 실패: {0}" }, "post_status": { "new_status": "새 게시물 게시", @@ -56,10 +98,13 @@ "account_not_locked_warning_link": "잠김", "attachments_sensitive": "첨부물을 민감함으로 설정", "content_type": { - "text/plain": "평문" + "text/plain": "평문", + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML" }, "content_warning": "주제 (필수 아님)", - "default": "LA에 도착!", + "default": "인천공항에 도착했습니다.", "direct_warning": "이 게시물을 멘션 된 사용자들에게만 보여집니다", "posting": "게시", "scope": { @@ -67,7 +112,15 @@ "private": "팔로워 전용 - 팔로워들에게만", "public": "공개 - 공개 타임라인으로", "unlisted": "비공개 - 공개 타임라인에 게시 안 함" - } + }, + "preview_empty": "아무것도 없습니다", + "preview": "미리보기", + "scope_notice": { + "public": "이 글은 누구나 볼 수 있습니다" + }, + "media_description_error": "파일을 올리지 못하였습니다. 다시한번 시도하여 주십시오", + "empty_status_error": "글을 입력하십시오", + "media_description": "첨부파일 설명" }, "registration": { "bio": "소개", @@ -85,7 +138,9 @@ "password_required": "공백으로 둘 수 없습니다", "password_confirmation_required": "공백으로 둘 수 없습니다", "password_confirmation_match": "패스워드와 일치해야 합니다" - } + }, + "fullname_placeholder": "예: 김례인", + "username_placeholder": "예: lain" }, "settings": { "attachmentRadius": "첨부물", @@ -112,7 +167,7 @@ "data_import_export_tab": "데이터 불러오기 / 내보내기", "default_vis": "기본 공개 범위", "delete_account": "계정 삭제", - "delete_account_description": "계정과 메시지를 영구히 삭제.", + "delete_account_description": "데이터가 영구히 삭제되고 계정이 불활성화됩니다.", "delete_account_error": "계정을 삭제하는데 문제가 있습니다. 계속 발생한다면 인스턴스 관리자에게 문의하세요.", "delete_account_instructions": "계정 삭제를 확인하기 위해 아래에 패스워드 입력.", "export_theme": "프리셋 저장", @@ -156,7 +211,7 @@ "notification_visibility_repeats": "반복", "no_rich_text_description": "모든 게시물의 서식을 지우기", "hide_follows_description": "내가 팔로우하는 사람을 표시하지 않음", - "hide_followers_description": "나를 따르는 사람을 보여주지 마라.", + "hide_followers_description": "나를 따르는 사람을 숨기기", "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화", "oauth_tokens": "OAuth 토큰", "token": "토큰", @@ -247,7 +302,16 @@ "borders": "테두리", "buttons": "버튼", "inputs": "입력칸", - "faint_text": "흐려진 텍스트" + "faint_text": "흐려진 텍스트", + "chat": { + "border": "경계선", + "outgoing": "송신", + "incoming": "수신" + }, + "selectedMenu": "선택된 메뉴 요소", + "selectedPost": "선택된 글", + "icons": "아이콘", + "alert_warning": "경고" }, "radii": { "_tab_label": "둥글기" @@ -303,14 +367,45 @@ "button": "버튼", "text": "더 많은 {0} 그리고 {1}", "mono": "내용", - "input": "LA에 막 도착!", + "input": "인천공항에 도착했습니다.", "faint_link": "도움 되는 설명서", "fine_print": "우리의 {0} 를 읽고 도움 되지 않는 것들을 배우자!", "header_faint": "이건 괜찮아", "checkbox": "나는 약관을 대충 훑어보았습니다", "link": "작고 귀여운 링크" } - } + }, + "block_export": "차단 목록 내보내기", + "mfa": { + "scan": { + "secret_code": "키", + "title": "스캔" + }, + "authentication_methods": "인증 방법", + "waiting_a_recovery_codes": "예비 코드를 수신하고 있습니다…", + "recovery_codes": "복구 코드.", + "generate_new_recovery_codes": "새로운 복구 코드를 작성", + "title": "2단계인증", + "confirm_and_enable": "OTP 확인과 활성화", + "setup_otp": "OTP 설치", + "otp": "OTP" + }, + "security": "보안", + "emoji_reactions_on_timeline": "이모지 반응을 타임라인으로 표시", + "avatar_size_instruction": "크기를 150x150 이상으로 설정할 것을 추장합니다.", + "blocks_tab": "차단", + "notification_setting_privacy": "보안", + "user_mutes": "사용자", + "notification_visibility_emoji_reactions": "반응", + "profile_fields": { + "value": "내용" + }, + "mutes_and_blocks": "침묵과 차단", + "chatMessageRadius": "챗 메시지", + "change_email": "전자메일 주소 바꾸기", + "changed_email": "메일주소가 갱신되었습니다!", + "bot": "이 계정은 bot입니다", + "mutes_tab": "침묵" }, "timeline": { "collapse": "접기", @@ -339,7 +434,7 @@ "its_you": "당신입니다!", "mute": "침묵", "muted": "침묵 됨", - "per_day": " / 하루", + "per_day": "/ 하루", "remote_follow": "원격 팔로우", "statuses": "게시물" }, @@ -357,11 +452,11 @@ "favorite": "즐겨찾기", "user_settings": "사용자 설정" }, - "upload":{ + "upload": { "error": { - "base": "업로드 실패.", - "file_too_big": "파일이 너무 커요 [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "잠시 후에 다시 시도해 보세요" + "base": "업로드 실패.", + "file_too_big": "파일이 너무 커요 [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "잠시 후에 다시 시도해 보세요" }, "file_size_units": { "B": "바이트", @@ -370,5 +465,122 @@ "GiB": "기비바이트", "TiB": "테비바이트" } + }, + "interactions": { + "follows": "새 팔로워", + "favs_repeats": "반복과 즐겨찾기" + }, + "emoji": { + "load_all": "전체 {emojiAmount} 이모지 불러오기", + "unicode": "Unicode 이모지", + "custom": "전용 이모지", + "add_emoji": "이모지 넣기", + "search_emoji": "이모지 검색", + "emoji": "이모지", + "stickers": "스티커" + }, + "polls": { + "add_poll": "투표를 추가", + "votes": "표", + "vote": "투표", + "type": "투표 형식", + "expiry": "투표 기간", + "votes_count": "{count} 표 | {count} 표", + "people_voted_count": "{count} 명 투표 | {count} 명 투표", + "option": "선택지", + "add_option": "선택지 추가" + }, + "media_modal": { + "next": "다음", + "previous": "이전" + }, + "importer": { + "error": "이 파일을 가져올 때 오류가 발생하였습니다.", + "success": "정상히 불러왔습니다.", + "submit": "보내기" + }, + "image_cropper": { + "cancel": "취소", + "save_without_cropping": "그대로 저장", + "save": "저장", + "crop_picture": "사진 자르기" + }, + "exporter": { + "processing": "처리중입니다, 처리가 끝나면 파일을 다운로드하라는 지시가 있겠습니다", + "export": "내보내기" + }, + "domain_mute_card": { + "unmute_progress": "침묵을 해제중…", + "unmute": "침묵 해제", + "mute_progress": "침묵으로 설정중…", + "mute": "침묵" + }, + "about": { + "staff": "운영자", + "mrf": { + "simple": { + "media_nsfw_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고에 붙혀 있는 매체는 민감함으로 설정됩니다:", + "media_nsfw": "매체를 민감함으로 설정", + "media_removal_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고에 붙혀 있는 매체는 제거됩니다:", + "media_removal": "매체 제거", + "ftl_removal_desc": "이 인스턴스에서 아래의 인스턴스들은 \"알려진 모든 네트워크\" 타임라인에서 제외됩니다:", + "ftl_removal": "\"알려진 모든 네트워크\" 타임라인에서 제외", + "quarantine_desc": "이 인스턴스는 아래의 인스턴스에게 공개투고만을 보냅니다:", + "quarantine": "검역", + "reject_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고를 받아들이지 않습니다:", + "accept_desc": "이 인스턴스에서는 아래의 인스턴스로부터 보내온 투고만이 접수됩니다:", + "reject": "거부", + "accept": "허가", + "simple_policies": "인스턴스 특유의 폴리시" + }, + "mrf_policies": "사용되는 MRF 폴리시", + "keyword": { + "is_replaced_by": "→", + "replace": "바꾸기", + "reject": "거부", + "ftl_removal": "\"알려진 모든 네트워크\" 타임라인에서 제외", + "keyword_policies": "단어 폴리시" + }, + "federation": "연합" + } + }, + "shoutbox": { + "title": "Shoutbox" + }, + "time": { + "years_short": "{0} 년", + "year_short": "{0} 년", + "years": "{0} 년", + "year": "{0} 년", + "weeks_short": "{0} 주일", + "week_short": "{0} 주일", + "weeks": "{0} 주일", + "week": "{0} 주일", + "seconds_short": "{0} 초", + "second_short": "{0} 초", + "seconds": "{0} 초", + "second": "{0} 초", + "now_short": "방금", + "now": "방끔", + "months_short": "{0} 달 전", + "month_short": "{0} 달 전", + "months": "{0} 달 전", + "month": "{0} 달 전", + "minutes_short": "{0} 분", + "minute_short": "{0} 분", + "minutes": "{0} 분", + "minute": "{0} 분", + "in_past": "{0} 전", + "hours_short": "{0} 시간", + "hour_short": "{0} 시간", + "hours": "{0} 시간", + "hour": "{0} 시간", + "days_short": "{0} 일", + "day_short": "{0} 일", + "days": "{0} 일", + "day": "{0} 일" + }, + "remote_user_resolver": { + "error": "찾을 수 없습니다." } } diff --git a/src/i18n/nb.json b/src/i18n/nb.json @@ -57,9 +57,9 @@ "enter_recovery_code": "Skriv inn en gjenopprettingskode", "enter_two_factor_code": "Skriv inn en to-faktors kode", "recovery_code": "Gjenopprettingskode", - "heading" : { - "totp" : "To-faktors autentisering", - "recovery" : "To-faktors gjenoppretting" + "heading": { + "totp": "To-faktors autentisering", + "recovery": "To-faktors gjenoppretting" } }, "media_modal": { @@ -72,7 +72,7 @@ "chat": "Lokal nettprat", "friend_requests": "Følgeforespørsler", "mentions": "Nevnt", - "interactions": "Interaksjooner", + "interactions": "Interaksjoner", "dms": "Direktemeldinger", "public_tl": "Offentlig Tidslinje", "timeline": "Tidslinje", @@ -80,7 +80,9 @@ "user_search": "Søk etter brukere", "search": "Søk", "who_to_follow": "Kontoer å følge", - "preferences": "Innstillinger" + "preferences": "Innstillinger", + "timelines": "Tidslinjer", + "bookmarks": "Bokmerker" }, "notifications": { "broken_favorite": "Ukjent status, leter etter den...", @@ -90,7 +92,8 @@ "notifications": "Varslinger", "read": "Les!", "repeated_you": "Gjentok din status", - "no_more_notifications": "Ingen gjenstående varsler" + "no_more_notifications": "Ingen gjenstående varsler", + "follow_request": "ønsker å følge deg" }, "polls": { "add_poll": "Legg til undersøkelse", @@ -134,7 +137,7 @@ "public": "Denne statusen vil være synlig for alle", "private": "Denne statusen vil være synlig for dine følgere", "unlisted": "Denne statusen vil ikke være synlig i Offentlig Tidslinje eller Det Hele Kjente Nettverket" - }, + }, "scope": { "direct": "Direkte, publiser bare til nevnte brukere", "private": "Bare følgere, publiser bare til brukere som følger deg", @@ -171,17 +174,17 @@ "security": "Sikkerhet", "enter_current_password_to_confirm": "Skriv inn ditt nåverende passord for å bekrefte din identitet", "mfa": { - "otp" : "OTP", - "setup_otp" : "Set opp OTP", - "wait_pre_setup_otp" : "forhåndsstiller OTP", - "confirm_and_enable" : "Bekreft og slå på OTP", + "otp": "OTP", + "setup_otp": "Set opp OTP", + "wait_pre_setup_otp": "forhåndsstiller OTP", + "confirm_and_enable": "Bekreft og slå på OTP", "title": "To-faktors autentisering", - "generate_new_recovery_codes" : "Generer nye gjenopprettingskoder", - "warning_of_generate_new_codes" : "Når du genererer nye gjenopprettingskoder, vil de gamle slutte å fungere.", - "recovery_codes" : "Gjenopprettingskoder.", + "generate_new_recovery_codes": "Generer nye gjenopprettingskoder", + "warning_of_generate_new_codes": "Når du genererer nye gjenopprettingskoder, vil de gamle slutte å fungere.", + "recovery_codes": "Gjenopprettingskoder.", "waiting_a_recovery_codes": "Mottar gjenopprettingskoder...", - "recovery_codes_warning" : "Skriv disse kodene ned eller plasser dem ett sikkert sted - ellers så vil du ikke se dem igjen. Dersom du mister tilgang til din to-faktors app og dine gjenopprettingskoder, vil du bli stengt ute av kontoen din.", - "authentication_methods" : "Autentiseringsmetoder", + "recovery_codes_warning": "Skriv disse kodene ned eller plasser dem ett sikkert sted - ellers så vil du ikke se dem igjen. Dersom du mister tilgang til din to-faktors app og dine gjenopprettingskoder, vil du bli stengt ute av kontoen din.", + "authentication_methods": "Autentiseringsmetoder", "scan": { "title": "Skann", "desc": "Ved hjelp av din to-faktors applikasjon, skann denne QR-koden eller skriv inn tekstnøkkelen", @@ -579,7 +582,7 @@ "favorite": "Lik", "user_settings": "Brukerinnstillinger" }, - "upload":{ + "upload": { "error": { "base": "Det oppsto en feil under opplastning.", "file_too_big": "Fil for stor [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", diff --git a/src/i18n/pt.json b/src/i18n/pt.json @@ -5,37 +5,66 @@ "features_panel": { "chat": "Chat", "gopher": "Gopher", - "media_proxy": "Proxy de mídia", + "media_proxy": "Proxy de multimédia", "scope_options": "Opções de privacidade", "text_limit": "Limite de caracteres", - "title": "Funções", - "who_to_follow": "Quem seguir" + "title": "Características", + "who_to_follow": "Quem seguir", + "upload_limit": "Limite de carregamento", + "pleroma_chat_messages": "Chat do Pleroma" }, "finder": { - "error_fetching_user": "Erro ao procurar usuário", - "find_user": "Buscar usuário" + "error_fetching_user": "Erro ao pesquisar utilizador", + "find_user": "Pesquisar utilizador" }, "general": { "apply": "Aplicar", "submit": "Enviar", "more": "Mais", - "generic_error": "Houve um erro", - "optional": "opcional" + "generic_error": "Ocorreu um erro", + "optional": "opcional", + "peek": "Espreitar", + "close": "Fechar", + "verify": "Verificar", + "confirm": "Confirmar", + "enable": "Ativar", + "disable": "Desativar", + "cancel": "Cancelar", + "show_less": "Mostrar menos", + "show_more": "Mostrar mais", + "retry": "Tenta novamente", + "error_retry": "Por favor, tenta novamente", + "loading": "A carregar…", + "dismiss": "Ignorar", + "role": + { + "moderator": "Moderador", + "admin": "Admin" + } }, "image_cropper": { "crop_picture": "Cortar imagem", - "save": "Salvar", - "cancel": "Cancelar" + "save": "Guardar", + "cancel": "Cancelar", + "save_without_cropping": "Guardar sem recortar" }, "login": { - "login": "Entrar", - "description": "Entrar com OAuth", - "logout": "Sair", - "password": "Senha", - "placeholder": "p.e. lain", - "register": "Registrar", - "username": "Usuário", - "hint": "Entre para participar da discussão" + "login": "Iniciar Sessão", + "description": "Iniciar sessão com OAuth", + "logout": "Terminar sessão", + "password": "Palavra-passe", + "placeholder": "ex. lain", + "register": "Registar", + "username": "Nome de Utilizador", + "hint": "Entra para participar na discussão", + "heading": { + "totp": "Autenticação de dois fatores", + "recovery": "Recuperação de dois fatores" + }, + "recovery_code": "Código de recuperação", + "authentication_code": "Código de autenticação", + "enter_two_factor_code": "Introduza o código de dois fatores", + "enter_recovery_code": "Introduza um código de recuperação" }, "media_modal": { "previous": "Anterior", @@ -45,100 +74,125 @@ "about": "Sobre", "back": "Voltar", "chat": "Chat local", - "friend_requests": "Solicitações de seguidores", + "friend_requests": "Pedidos de seguidores", "mentions": "Menções", - "dms": "Mensagens diretas", - "public_tl": "Linha do tempo pública", - "timeline": "Linha do tempo", - "twkn": "Toda a rede conhecida", - "user_search": "Buscar usuários", + "dms": "Mensagens Diretas", + "public_tl": "Cronologia Pública", + "timeline": "Cronologia", + "twkn": "Rede conhecida", + "user_search": "Pesquisa por Utilizadores", "who_to_follow": "Quem seguir", - "preferences": "Preferências" + "preferences": "Preferências", + "search": "Pesquisar", + "interactions": "Interações", + "administration": "Administração", + "chats": "Salas de Chat", + "timelines": "Cronologias", + "bookmarks": "Itens Guardados" }, "notifications": { - "broken_favorite": "Status desconhecido, buscando...", - "favorited_you": "favoritou sua postagem", - "followed_you": "seguiu você", + "broken_favorite": "Publicação desconhecida, a procurar…", + "favorited_you": "gostou do teu post", + "followed_you": "seguiu-te", "load_older": "Carregar notificações antigas", "notifications": "Notificações", "read": "Lido!", - "repeated_you": "repetiu sua postagem", - "no_more_notifications": "Mais nenhuma notificação" + "repeated_you": "partilhou o teu post", + "no_more_notifications": "Sem mais notificações", + "reacted_with": "reagiu com {0}", + "migrated_to": "migrou para", + "follow_request": "quer seguir-te", + "error": "Erro ao obter notificações: {0}" }, "post_status": { - "new_status": "Postar novo status", - "account_not_locked_warning": "Sua conta não é {0}. Qualquer pessoa pode te seguir e ver seus posts privados (só para seguidores).", - "account_not_locked_warning_link": "restrita", + "new_status": "Publicar nova publicação", + "account_not_locked_warning": "A sua conta não é {0}. Qualquer pessoa pode seguir-te e ver os seus posts privados (só para seguidores).", + "account_not_locked_warning_link": "restrito", "attachments_sensitive": "Marcar anexos como sensíveis", "content_type": { - "text/plain": "Texto puro" + "text/plain": "Texto puro", + "text/bbcode": "BBCode", + "text/html": "HTML", + "text/markdown": "Remarcação" }, "content_warning": "Assunto (opcional)", - "default": "Acabei de chegar no Rio!", + "default": "Acabei de chegar a Lisboa.", "direct_warning": "Este post será visível apenas para os usuários mencionados.", - "posting": "Publicando", + "posting": "A publicar", "scope": { "direct": "Direto - Enviar somente aos usuários mencionados", "private": "Apenas para seguidores - Enviar apenas para seguidores", - "public": "Público - Enviar a linhas do tempo públicas", - "unlisted": "Não listado - Não enviar a linhas do tempo públicas" - } + "public": "Público - Publicar em cronologias públicas", + "unlisted": "Não listado - Não exibir em cronologias públicas" + }, + "scope_notice": { + "unlisted": "Esta publicação não será visível na Cronologia pública e na Rede conhecida por todos", + "private": "Esta publicação será apenas visível para os teus seguidores", + "public": "Esta publicação será visível para todos" + }, + "empty_status_error": "Não consegues publicar um post vazio e sem ficheiros", + "preview_empty": "Vazio", + "preview": "Pré-visualização", + "media_description": "Descrição da multimédia", + "media_description_error": "Falha ao atualizar ficheiro, tente novamente", + "direct_warning_to_first_only": "Esta publicação só será visível para os utilizadores mencionados no início da mensagem.", + "direct_warning_to_all": "Esta publicação será visível para todos os utilizadores mencionados." }, "registration": { "bio": "Biografia", - "email": "Correio eletrônico", + "email": "Endereço de e-mail", "fullname": "Nome para exibição", - "password_confirm": "Confirmação de senha", - "registration": "Registro", + "password_confirm": "Confirmação de palavra-passe", + "registration": "Registo", "token": "Código do convite", "captcha": "CAPTCHA", "new_captcha": "Clique na imagem para carregar um novo captcha", - "username_placeholder": "p. ex. lain", - "fullname_placeholder": "p. ex. Lain Iwakura", - "bio_placeholder": "e.g.\nOi, sou Lain\nSou uma garota que vive no subúrbio do Japão. Você deve me conhecer da Rede.", + "username_placeholder": "ex. lain", + "fullname_placeholder": "ex. Lain Iwakura", + "bio_placeholder": "ex.\nOlá, sou a Lain\nSou uma menina de anime que vive no Japão suburbano. Devem conhecer-me do \"the Wired\".", "validations": { "username_required": "não pode ser deixado em branco", "fullname_required": "não pode ser deixado em branco", "email_required": "não pode ser deixado em branco", "password_required": "não pode ser deixado em branco", "password_confirmation_required": "não pode ser deixado em branco", - "password_confirmation_match": "deve ser idêntica à senha" + "password_confirmation_match": "deve corresponder à palavra-passe" } }, "settings": { - "app_name": "Nome do aplicativo", + "app_name": "Nome da aplicação", "attachmentRadius": "Anexos", "attachments": "Anexos", "avatar": "Avatar", "avatarAltRadius": "Avatares (Notificações)", "avatarRadius": "Avatares", - "background": "Pano de Fundo", + "background": "Imagem de Fundo", "bio": "Biografia", "blocks_tab": "Bloqueios", "btnRadius": "Botões", "cBlue": "Azul (Responder, seguir)", - "cGreen": "Verde (Repetir)", + "cGreen": "Verde (Partilhar)", "cOrange": "Laranja (Favoritar)", "cRed": "Vermelho (Cancelar)", - "change_password": "Mudar senha", - "change_password_error": "Houve um erro ao modificar sua senha.", - "changed_password": "Senha modificada com sucesso!", + "change_password": "Mudar palavra-passe", + "change_password_error": "Ocorreu um erro ao modificar a sua palavra-passe.", + "changed_password": "Palavra-passe modificada com sucesso!", "collapse_subject": "Esconder posts com assunto", "composing": "Escrita", - "confirm_new_password": "Confirmar nova senha", + "confirm_new_password": "Confirmar nova palavra-passe", "current_avatar": "Seu avatar atual", - "current_password": "Sua senha atual", + "current_password": "Palavra-passe atual", "current_profile_banner": "Sua capa de perfil atual", "data_import_export_tab": "Importação/exportação de dados", "default_vis": "Opção de privacidade padrão", - "delete_account": "Deletar conta", - "delete_account_description": "Deletar sua conta e mensagens permanentemente.", - "delete_account_error": "Houve um problema ao deletar sua conta. Se ele persistir, por favor entre em contato com o/a administrador/a da instância.", - "delete_account_instructions": "Digite sua senha no campo abaixo para confirmar a exclusão da conta.", + "delete_account": "Eliminar conta", + "delete_account_description": "Apagar os seus dados permanentemente e desativar a sua conta.", + "delete_account_error": "Ocorreu um erro ao remover a sua conta. Se este persistir, por favor entre em contato com o/a administrador/a da instância.", + "delete_account_instructions": "Escreva a sua palavra-passe no campo abaixo para confirmar a remoção da conta.", "avatar_size_instruction": "O tamanho mínimo recomendado para imagens de avatar é 150x150 pixels.", - "export_theme": "Salvar predefinições", + "export_theme": "Guardar predefinições", "filtering": "Filtragem", - "filtering_explanation": "Todas as postagens contendo estas palavras serão silenciadas; uma palavra por linha.", + "filtering_explanation": "Todas as publicações que contenham estas palavras serão silenciadas; uma palavra por linha", "follow_export": "Exportar quem você segue", "follow_export_button": "Exportar quem você segue para um arquivo CSV", "follow_export_processing": "Processando. Em breve você receberá a solicitação de download do arquivo", @@ -148,7 +202,7 @@ "foreground": "Primeiro Plano", "general": "Geral", "hide_attachments_in_convo": "Ocultar anexos em conversas", - "hide_attachments_in_tl": "Ocultar anexos na linha do tempo.", + "hide_attachments_in_tl": "Ocultar anexos na cronologia", "max_thumbnails": "Número máximo de miniaturas por post", "hide_isp": "Esconder painel específico da instância", "preload_images": "Pré-carregar imagens", @@ -159,7 +213,7 @@ "import_followers_from_a_csv_file": "Importe seguidores a partir de um arquivo CSV", "import_theme": "Carregar pré-definição", "inputRadius": "Campos de entrada", - "checkboxRadius": "Checkboxes", + "checkboxRadius": "Caixas de seleção", "instance_default": "(padrão: {value})", "instance_default_simple": "(padrão)", "interface": "Interface", @@ -171,16 +225,16 @@ "loop_video": "Repetir vídeos", "loop_video_silent_only": "Repetir apenas vídeos sem som (como os \"gifs\" do Mastodon)", "mutes_tab": "Silenciados", - "play_videos_in_modal": "Tocar vídeos diretamente no visualizador de mídia", + "play_videos_in_modal": "Reproduzir vídeos diretamente no visualizador de multimédia", "use_contain_fit": "Não cortar o anexo na miniatura", "name": "Nome", "name_bio": "Nome & Biografia", - "new_password": "Nova senha", + "new_password": "Nova palavra-passe", "notification_visibility": "Tipos de notificação para mostrar", "notification_visibility_follows": "Seguidas", "notification_visibility_likes": "Favoritos", "notification_visibility_mentions": "Menções", - "notification_visibility_repeats": "Repetições", + "notification_visibility_repeats": "Partilhas", "no_rich_text_description": "Remover formatação de todos os posts", "no_blocks": "Sem bloqueios", "no_mutes": "Sem silenciados", @@ -188,7 +242,7 @@ "hide_followers_description": "Não mostrar quem me segue", "show_admin_badge": "Mostrar título de Administrador em meu perfil", "show_moderator_badge": "Mostrar título de Moderador em meu perfil", - "nsfw_clickthrough": "Habilitar clique para ocultar anexos sensíveis", + "nsfw_clickthrough": "Ativar clique em anexos e pré-visualizações de links para ocultar anexos NSFW", "oauth_tokens": "Token OAuth", "token": "Token", "refresh_token": "Atualizar Token", @@ -201,7 +255,7 @@ "profile_banner": "Capa de perfil", "profile_tab": "Perfil", "radii_help": "Arredondar arestas da interface (em pixel)", - "replies_in_timeline": "Respostas na linha do tempo", + "replies_in_timeline": "Respostas na cronologia", "reply_visibility_all": "Mostrar todas as respostas", "reply_visibility_following": "Só mostrar respostas direcionadas a mim ou a usuários que sigo", "reply_visibility_self": "Só mostrar respostas direcionadas a mim", @@ -215,7 +269,7 @@ "settings": "Configurações", "subject_input_always_show": "Sempre mostrar campo de assunto", "subject_line_behavior": "Copiar assunto ao responder", - "subject_line_email": "Como em email: \"re: assunto\"", + "subject_line_email": "Como num e-mail: \"re: assunto\"", "subject_line_mastodon": "Como o Mastodon: copiar como está", "subject_line_noop": "Não copiar", "post_status_content_type": "Tipo de conteúdo do status", @@ -225,7 +279,7 @@ "theme": "Tema", "theme_help": "Use cores em código hexadecimal (#rrggbb) para personalizar seu esquema de cores.", "theme_help_v2_1": "Você também pode sobrescrever as cores e opacidade de alguns componentes ao modificar o checkbox, use \"Limpar todos\" para limpar todas as modificações.", - "theme_help_v2_2": "Alguns ícones sob registros são indicadores de fundo/contraste de textos, passe por cima para informações detalhadas. Tenha ciência de que os indicadores de contraste não funcionam muito bem com transparência.", + "theme_help_v2_2": "Alguns ícones em registo são indicadores de fundo/contraste de textos, passe por cima para obter informações detalhadas. Tenha em atenção que os indicadores de contraste não funcionam muito bem com transparência.", "tooltipRadius": "Dicas/alertas", "upload_a_photo": "Enviar uma foto", "user_settings": "Configurações de Usuário", @@ -245,7 +299,24 @@ "save_load_hint": "Manter as opções preserva as opções atuais ao selecionar ou carregar temas; também salva as opções ao exportar um tempo. Quanto todos os campos estiverem desmarcados, tudo será salvo ao exportar o tema.", "reset": "Restaurar o padrão", "clear_all": "Limpar tudo", - "clear_opacity": "Limpar opacidade" + "clear_opacity": "Limpar opacidade", + "help": { + "upgraded_from_v2": "O PleromaFE foi atualizado, a aparência do tema poderá ser um pouco diferente.", + "snapshot_source_mismatch": "Conflito de versões: o mais provável é que o FE tenha revertido e voltado a atualizar, foi alterado o tema numa versão anterior do FE, o mais provável é desejar utilizar a versão anterior; caso contrário, utilize a nova versão.", + "migration_napshot_gone": "Por algum motivo, a pré-visualização estava em falta, algumas coisas poderão parecer diferentes do que se lembra.", + "migration_snapshot_ok": "Para estar seguro, foi carregada uma versão de pré-visualização do tema. Pode tentar carregar dados do tema.", + "fe_downgraded": "Versão do PleromaFE revertida.", + "fe_upgraded": "O criador de temas do PleromaFE foi atualizado depois da atualização da versão.", + "snapshot_missing": "Não existia nenhuma pré-visualização do tema no ficheiro, então pode parecer diferente do previsto originalmente.", + "snapshot_present": "Foi carregada uma pré-visualização do tema, todos os valores são substituídos. Caso contrário, pode carregar o tema completo.", + "older_version_imported": "O ficheiro que importaste foi criado numa versão antiga do FE.", + "future_version_imported": "O ficheiro que importaste foi criado para uma versão mais recente do FE.", + "v2_imported": "O ficheiro que importaste foi feito para uma versão antiga do FE. Tentamos maximizar a compatibilidade, porém, poderão existir incongruências." + }, + "use_source": "Nova versão", + "use_snapshot": "Versão antiga", + "keep_as_is": "Manter como está", + "load_theme": "Carregar tema" }, "common": { "color": "Cor", @@ -280,7 +351,27 @@ "borders": "Bordas", "buttons": "Botões", "inputs": "Caixas de entrada", - "faint_text": "Texto esmaecido" + "faint_text": "Texto esmaecido", + "chat": { + "border": "Borda", + "outgoing": "Enviadas", + "incoming": "Recebidas" + }, + "tabs": "Abas", + "toggled": "Alternado", + "disabled": "Desativado", + "selectedMenu": "Elemento do menu seleccionado", + "selectedPost": "Publicação seleccionada", + "pressed": "Pressionado", + "highlight": "Elementos destacados", + "icons": "Ícones", + "poll": "Gráfico da sondagem", + "wallpaper": "Fundo de ecrã", + "underlay": "Sublinhado", + "popover": "Sugestões, menus, etiquetas", + "post": "Publicações/Bios", + "alert_neutral": "Neutro", + "alert_warning": "Precaução" }, "radii": { "_tab_label": "Arredondado" @@ -298,7 +389,7 @@ "always_drop_shadow": "Atenção, esta sombra sempre utiliza {0} quando compatível com o navegador.", "drop_shadow_syntax": "{0} não é compatível com o parâmetro {1} e a palavra-chave {2}.", "avatar_inset": "Tenha em mente que combinar as sombras de inserção e a não-inserção em avatares pode causar resultados inesperados em avatares transparentes.", - "spread_zero": "Sombras com uma difusão > 0 aparecerão como se fossem definidas como 0.", + "spread_zero": "Sombras com difusão > 0 aparecerão como se fossem definidas como zero", "inset_classic": "Sombras de inserção utilizarão {0}" }, "components": { @@ -313,7 +404,8 @@ "buttonPressed": "Botão (pressionado)", "buttonPressedHover": "Botão (pressionado+em cima)", "input": "Campo de entrada" - } + }, + "hintV3": "Para as sombras, também pode usar a notação {0} para usar outro espaço de cor." }, "fonts": { "_tab_label": "Fontes", @@ -336,30 +428,143 @@ "button": "Botão", "text": "Vários {0} e {1}", "mono": "conteúdo", - "input": "Acabei de chegar no Rio!", + "input": "Acabei de chegar a Lisboa.", "faint_link": "manual útil", "fine_print": "Leia nosso {0} para não aprender nada!", - "header_faint": "Está ok!", + "header_faint": "Isto está bem", "checkbox": "Li os termos e condições", "link": "um belo link" } - } + }, + "mfa": { + "scan": { + "secret_code": "Chave", + "title": "Scan", + "desc": "Utilizando a sua aplicação de dois fatores, faça scan deste código QR ou insira a chave de texto:" + }, + "authentication_methods": "Métodos de autenticação", + "recovery_codes": "Códigos de recuperação.", + "generate_new_recovery_codes": "Gerar novos códigos de recuperação", + "confirm_and_enable": "Confirmar e ativar a palavra-passe de utilização única", + "otp": "Palavra-passe de utilização única", + "verify": { + "desc": "Para ativar a autenticação de dois fatores, introduza o código da sua aplicação de dois fatores:" + }, + "recovery_codes_warning": "Anote os códigos ou armazene-os num lugar seguro - caso contrário, não os voltará a ver. Se perder acesso à sua aplicação de dois fatores e aos códigos de recuperação, a sua conta ficará bloqueada.", + "waiting_a_recovery_codes": "A receber códigos de recuperação…", + "warning_of_generate_new_codes": "Quando gera novos códigos de recuperação, os antigos deixam de funcionar.", + "title": "Autenticação de Dois Fatores", + "wait_pre_setup_otp": "pré-configuração de palavra-passe de utilização única", + "setup_otp": "Configurar palavra-passe de utilização única" + }, + "security": "Segurança", + "mute_import_error": "Erro ao importar os silenciados", + "mute_import": "Importar silenciados", + "mute_export_button": "Exporta os silenciados para um ficheiro csv", + "mute_export": "Exportar silenciados", + "blocks_imported": "Lista de utilizadores bloqueados importada! O processo pode demorar alguns instantes.", + "block_import_error": "Erro ao importar a lista de utilizadores bloqueados", + "block_import": "Importar utilizadores bloqueados", + "block_export_button": "Exporta a tua lista de utilizadores bloqueados para um ficheiro csv", + "block_export": "Exportar utilizadores bloqueados", + "enter_current_password_to_confirm": "Introduza a sua palavra-passe atual para confirmar a sua identidade", + "mutes_and_blocks": "Silenciados e Bloqueados", + "chatMessageRadius": "Mensagem de texto", + "changed_email": "Endereço de e-mail modificado com sucesso!", + "change_email_error": "Ocorreu um erro ao modificar o seu endereço de e-mail.", + "change_email": "Mudar Endereço de E-mail", + "bot": "Esta uma conta robô", + "import_mutes_from_a_csv_file": "Importar silenciados de um ficheiro csv", + "mutes_imported": "Silenciados importados! Processá-los pode demorar alguns instantes.", + "allow_following_move": "Permitir seguimento automático quando a conta for migrada para outra instância", + "domain_mutes": "Domínios", + "discoverable": "Permitir a descoberta desta conta em resultados de busca e outros serviços", + "emoji_reactions_on_timeline": "Mostrar reações de emoji na timeline", + "hide_muted_posts": "Esconder posts de utilizadores silenciados", + "hide_follows_count_description": "Não mostrar o número de contas seguidas", + "hide_followers_count_description": "Não mostrar o número de seguidores", + "notification_visibility_emoji_reactions": "Reações", + "new_email": "Novo endereço de e-mail", + "profile_fields": { + "value": "Conteúdo", + "add_field": "Adicionar campo", + "label": "Metadados do perfil", + "name": "Etiqueta" + }, + "import_blocks_from_a_csv_file": "Importar bloqueados a partir de um arquivo CSV", + "hide_wallpaper": "Esconder papel de parede da instância", + "notification_setting_privacy": "Privacidade", + "notification_setting_filters": "Filtros", + "fun": "Divertido", + "user_mutes": "Utilizadores", + "type_domains_to_mute": "Pesquisar domínios para silenciar", + "useStreamingApiWarning": "(não recomendado, experimental, pode omitir publicações)", + "useStreamingApi": "Receber publicações e notificações em tempo real", + "minimal_scopes_mode": "Minimizar as opções de publicação", + "search_user_to_mute": "Pesquisar utilizadores que pretende silenciar", + "search_user_to_block": "Pesquisa quais utilizadores desejas bloquear", + "notification_setting_hide_notification_contents": "Ocultar o remetente e o conteúdo das notificações push", + "version": { + "frontend_version": "Versão do Frontend", + "backend_version": "Versão do Backend", + "title": "Versão" + }, + "notification_blocks": "Bloquear um utilizador previne todas as notificações, bem como as desativa.", + "notification_mutes": "Para deixar de receber notificações de um utilizador específico, silencia-o.", + "notification_setting_block_from_strangers": "Bloqueia as notificações de utilizadores que não segues", + "greentext": "Texto verde (meme arrows)", + "virtual_scrolling": "Otimizar a apresentação da cronologia", + "reset_background_confirm": "Tens a certeza que desejas redefinir o fundo?", + "reset_banner_confirm": "Tens a certeza que desejas redefinir a imagem do cabeçalho?", + "reset_avatar_confirm": "Tens a certeza que desejas redefinir o avatar?", + "reset_profile_banner": "Redefinir imagem do cabeçalho do perfil", + "reset_profile_background": "Redefinir fundo de perfil", + "reset_avatar": "Redefinir avatar", + "autohide_floating_post_button": "Automaticamente ocultar o botão 'Nova Publicação' (telemóvel)", + "notification_visibility_moves": "Utilizador Migrado", + "accent": "Destaque", + "pad_emoji": "Preencher espaços ao adicionar emojis do seletor" }, "timeline": { "collapse": "Esconder", "conversation": "Conversa", "error_fetching": "Erro ao buscar atualizações", "load_older": "Carregar postagens antigas", - "no_retweet_hint": "Posts apenas para seguidores ou diretos não podem ser repetidos", - "repeated": "Repetido", + "no_retweet_hint": "Posts apenas para seguidores ou diretos não podem ser partilhados", + "repeated": "partilhado", "show_new": "Mostrar novas", "up_to_date": "Atualizado", "no_more_statuses": "Sem mais posts", - "no_statuses": "Sem posts" + "no_statuses": "Sem posts", + "reload": "Recarregar", + "error": "Erro a obter a cronologia: {0}" }, "status": { "reply_to": "Responder a", - "replies_list": "Respostas:" + "replies_list": "Respostas:", + "unbookmark": "Remover post dos Items Guardados", + "expand": "Expandir", + "nsfw": "NSFW (Não apropriado para trabalho)", + "status_deleted": "Esta publicação foi apagada", + "hide_content": "Ocultar o conteúdo", + "show_content": "Mostrar o conteúdo", + "hide_full_subject": "Ocultar o assunto completo", + "show_full_subject": "Mostrar o assunto completo", + "thread_muted_and_words": ", contém:", + "thread_muted": "Conversação silenciada", + "external_source": "Fonte externa", + "copy_link": "Copiar o link do post", + "status_unavailable": "Publicação indisponível", + "unmute_conversation": "Mostrar a conversação", + "mute_conversation": "Silenciar a conversação", + "delete_confirm": "Tens a certeza que desejas apagar a publicação?", + "bookmark": "Guardar", + "pin": "Fixar no perfil", + "pinned": "Afixado", + "unpin": "Desafixar do perfil", + "delete": "Eliminar publicação", + "repeats": "Partilhados", + "favorites": "Favoritos" }, "user_card": { "approve": "Aprovar", @@ -377,21 +582,48 @@ "following": "Seguindo!", "follows_you": "Segue você!", "its_you": "É você!", - "media": "Mídia", + "media": "Multimédia", "mute": "Silenciar", "muted": "Silenciado", "per_day": "por dia", "remote_follow": "Seguir remotamente", "statuses": "Postagens", "unblock": "Desbloquear", - "unblock_progress": "Desbloqueando...", - "block_progress": "Bloqueando...", + "unblock_progress": "A desbloquear…", + "block_progress": "A bloquear…", "unmute": "Retirar silêncio", - "unmute_progress": "Retirando silêncio...", - "mute_progress": "Silenciando..." + "unmute_progress": "A retirar silêncio…", + "mute_progress": "A silenciar…", + "admin_menu": { + "delete_user_confirmation": "Tens a certeza? Esta ação não pode ser revertida.", + "delete_user": "Eliminar utilizador", + "quarantine": "Não permitir publicações de utilizadores de instâncias remotas", + "disable_any_subscription": "Não permitir que nenhum utilizador te siga", + "disable_remote_subscription": "Não permitir seguidores de instâncias remotas", + "sandbox": "Forçar publicações apenas para seguidores", + "force_unlisted": "Forçar publicações como não listadas", + "strip_media": "Eliminar ficheiros multimédia das publicações", + "force_nsfw": "Marcar todas as publicações como NSFW (não apropriado para o trabalho)", + "delete_account": "Eliminar Conta", + "deactivate_account": "Desativar conta", + "activate_account": "Ativar conta", + "revoke_moderator": "Revogar permissões de Moderador", + "grant_moderator": "Conceder permissões de Moderador", + "revoke_admin": "Revogar permissões de Admin", + "grant_admin": "Conceder permissões de Admin", + "moderation": "Moderação" + }, + "show_repeats": "Mostrar partilhas", + "hide_repeats": "Ocultar partilhas", + "unsubscribe": "Retirar subscrição", + "subscribe": "Subscrever", + "report": "Denunciar", + "message": "Mensagem", + "mention": "Mencionar", + "hidden": "Ocultar" }, "user_profile": { - "timeline_title": "Linha do tempo do usuário", + "timeline_title": "Cronologia do Utilizador", "profile_does_not_exist": "Desculpe, este perfil não existe.", "profile_loading_error": "Desculpe, houve um erro ao carregar este perfil." }, @@ -400,17 +632,22 @@ "who_to_follow": "Quem seguir" }, "tool_tip": { - "media_upload": "Envio de mídia", - "repeat": "Repetir", + "media_upload": "Envio de multimédia", + "repeat": "Partilhar", "reply": "Responder", "favorite": "Favoritar", - "user_settings": "Configurações do usuário" + "user_settings": "Configurações do usuário", + "bookmark": "Guardar", + "reject_follow_request": "Rejeitar o pedido de seguimento", + "accept_follow_request": "Aceitar o pedido de seguimento", + "add_reaction": "Adicionar Reação" }, - "upload":{ + "upload": { "error": { "base": "Falha no envio.", "file_too_big": "Arquivo grande demais [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Tente novamente mais tarde" + "default": "Tente novamente mais tarde", + "message": "Falha ao enviar: {0}" }, "file_size_units": { "B": "B", @@ -419,5 +656,179 @@ "GiB": "GiB", "TiB": "TiB" } + }, + "about": { + "mrf": { + "simple": { + "quarantine": "Quarentena", + "reject": "Rejeitar", + "accept": "Aceitar", + "media_removal_desc": "Este domínio remove multimédia das publicações dos seguintes domínios:", + "media_removal": "Remoção de multimédia", + "ftl_removal_desc": "Este domínio remove os seguintes domínios da cronologia \"Rede conhecida por todos\":", + "quarantine_desc": "Este domínio apenas irá publicar nos seguintes domínios:", + "reject_desc": "Este domínio não aceitará mensagens dos seguintes domínios:", + "accept_desc": "Este domínio aceita apenas mensagens dos seguintes domínios:", + "simple_policies": "Políticas especificas do domínio", + "media_nsfw": "Forçar definição de multimédia como Sensível", + "ftl_removal": "Remoção da cronologia da \"Rede conhecida por todos\"", + "media_nsfw_desc": "Este domínio força a multimédia a ser marcada como sensível nos seguintes domínios:" + }, + "keyword": { + "replace": "Substituir", + "reject": "Rejeitar", + "is_replaced_by": "→", + "keyword_policies": "Política de Palavras-Chave", + "ftl_removal": "Remoção da cronologia da \"Rede conhecida por todos\"" + }, + "federation": "Federação", + "mrf_policies": "Ativar Políticas MRF", + "mrf_policies_desc": "Políticas MRF manipulam o comportamento da federação nos domínios. As seguintes políticas estão ativadas:" + }, + "staff": "Staff" + }, + "remote_user_resolver": { + "searching_for": "A pesquisar por", + "error": "Não encontrado.", + "remote_user_resolver": "Resolução de utilizador remoto" + }, + "emoji": { + "unicode": "Emoji Unicode", + "custom": "Emoji customizado", + "add_emoji": "Inserir emoji", + "search_emoji": "Pesquisar por um emoji", + "emoji": "Emoji", + "load_all": "A carregar todos os {emojiAmount} emojis", + "load_all_hint": "Carregado o primeiro emoji {saneAmount}, carregar todos os emojis pode causar problemas de desempenho.", + "keep_open": "Manter o seletor aberto", + "stickers": "Autocolantes" + }, + "polls": { + "single_choice": "Escolha única", + "vote": "Vota", + "votes": "votos", + "option": "Opção", + "add_option": "Adicionar Opção", + "not_enough_options": "Demasiado poucas opções únicas na sondagem", + "expired": "A sondagem terminou há {0}", + "expires_in": "A sondagem termina em {0}", + "expiry": "Tempo para finalizar sondagem", + "multiple_choices": "Escolha múltipla", + "type": "Tipo de sondagem", + "add_poll": "Adicionar Sondagem" + }, + "importer": { + "error": "Ocorreu um erro ao importar este ficheiro.", + "success": "Importado com sucesso.", + "submit": "Enviar" + }, + "exporter": { + "processing": "A processar, brevemente ser-te-á pedido que descarregues o ficheiro", + "export": "Exportar" + }, + "domain_mute_card": { + "mute_progress": "A silenciar…", + "mute": "Silenciar", + "unmute": "Remover silêncio", + "unmute_progress": "A remover o silêncio…" + }, + "selectable_list": { + "select_all": "Seleccionar tudo" + }, + "interactions": { + "load_older": "Carregar interações mais antigas", + "follows": "Novos seguidores", + "favs_repeats": "Gostos e Partilhas", + "moves": "O utilizador migra" + }, + "errors": { + "storage_unavailable": "O Pleroma não conseguiu aceder ao armazenamento do navegador. A sua sessão ou definições locais não serão armazenadas e poderá encontrar problemas inesperados. Tente ativar as cookies." + }, + "shoutbox": { + "title": "Chat Geral" + }, + "chats": { + "chats": "Chats", + "empty_chat_list_placeholder": "Não tens conversações ainda. Inicia uma nova conversa!", + "error_sending_message": "Ocorreu algo de errado ao enviar a mensagem.", + "error_loading_chat": "Ocorreu algo de errado ao carregar o chat.", + "delete_confirm": "Desejas realmente apagar esta mensagem?", + "more": "Mais", + "empty_message_error": "Não podes publicar uma mensagem vazia", + "new": "Nova conversação", + "delete": "Apagar", + "message_user": "Mensagem de {nickname}", + "you": "Tu:" + }, + "search": { + "hashtags": "Hashtags", + "no_results": "Sem resultados", + "person_talking": "{count} pessoa a falar", + "people_talking": "{0} pessoas a falar", + "people": "Pessoas" + }, + "display_date": { + "today": "Hoje" + }, + "file_type": { + "file": "Ficheiro", + "image": "Imagem", + "video": "Vídeo", + "audio": "Áudio" + }, + "password_reset": { + "password_reset_required_but_mailer_is_disabled": "Deves repor a tua palavra-passe, porém, a reposição de palavra-passe está desativada. Contacta o administrador da tua instância.", + "password_reset_required": "Deves repor a tua palavra-passe para iniciar sessão.", + "password_reset_disabled": "A reposição da palavra-passe foi desativada. Contacta o administrador da tua instância.", + "too_many_requests": "Alcançaste o limite de tentativas, tenta novamente mais tarde.", + "return_home": "Voltar à página principal", + "check_email": "Verifica o teu endereço de e-mail para obter um link para repor a tua palavra-passe.", + "placeholder": "O teu endereço de e-mail ou nome de utilizador", + "instruction": "Introduz o teu endereço de e-mail ou nome de utilizador. Enviaremos um link para repores a tua palavra-passe.", + "password_reset": "Repor palavra-passe", + "forgot_password": "Esqueceu-se da palavra-passe?" + }, + "user_reporting": { + "generic_error": "Ocorreu um erro ao processar o teu pedido.", + "submit": "Enviar", + "forward_to": "Encaminhar para {0}", + "forward_description": "A conta é de outro servidor. Enviar também uma cópia da denúncia à outra instância?", + "additional_comments": "Comentários adicionais", + "add_comment_description": "Esta denúncia será enviada aos moderadores desta instância. Podes fornecer uma explicação pela qual te encontras a denunciar esta conta abaixo:", + "title": "Denunciar {0}" + }, + "time": { + "years_short": "{0}a", + "year_short": "{0}a", + "years": "{0} anos", + "year": "{0} ano", + "weeks_short": "{0}sem", + "week_short": "{0}sem", + "weeks": "{0} semanas", + "week": "{0} semana", + "seconds_short": "{0}s", + "second_short": "{0}s", + "seconds": "{0} segundos", + "second": "{0} segundo", + "now": "agora mesmo", + "now_short": "agora", + "months_short": "{0}m", + "month_short": "{0}m", + "months": "{0} meses", + "month": "{0} mês", + "minutes_short": "{0}min", + "minute_short": "{0}min", + "minutes": "{0} minutos", + "minute": "{0} minuto", + "in_past": "há {0}", + "in_future": "em {0}", + "hours_short": "{0}h", + "hour_short": "{0}h", + "hours": "{0} horas", + "hour": "{0} hora", + "days_short": "{0}d", + "day_short": "{0}d", + "days": "{0} dias", + "day": "{0} dia" } } diff --git a/src/i18n/ru.json b/src/i18n/ru.json @@ -24,7 +24,11 @@ "retry": "Попробуйте еще раз", "error_retry": "Пожалуйста попробуйте еще раз", "close": "Закрыть", - "loading": "Загрузка…" + "loading": "Загрузка…", + "role": { + "moderator": "Модератор", + "admin": "Администратор" + } }, "login": { "login": "Войти", @@ -183,14 +187,14 @@ "change_password": "Сменить пароль", "change_password_error": "Произошла ошибка при попытке изменить пароль.", "changed_password": "Пароль изменён успешно!", - "collapse_subject": "Сворачивать посты с темой", + "collapse_subject": "Сворачивать статусы с темой", "confirm_new_password": "Подтверждение нового пароля", "current_avatar": "Текущий аватар", "current_password": "Текущий пароль", "current_profile_banner": "Текущий баннер профиля", "data_import_export_tab": "Импорт / Экспорт данных", "delete_account": "Удалить аккаунт", - "delete_account_description": "Удалить ваш аккаунт и все ваши сообщения.", + "delete_account_description": "Удалить вашу учётную запись и все ваши сообщения.", "delete_account_error": "Возникла ошибка в процессе удаления вашего аккаунта. Если это повторяется, свяжитесь с администратором вашего сервера.", "delete_account_instructions": "Введите ваш пароль в поле ниже для подтверждения удаления.", "export_theme": "Сохранить Тему", @@ -238,7 +242,7 @@ "hide_followers_count_description": "Не показывать число моих подписчиков", "show_admin_badge": "Показывать значок администратора в моем профиле", "show_moderator_badge": "Показывать значок модератора в моем профиле", - "nsfw_clickthrough": "Включить скрытие NSFW вложений и не показывать изображения в предпросмотре ссылок для NSFW статусов", + "nsfw_clickthrough": "Включить скрытие вложений и предпросмотра ссылок для NSFW статусов", "oauth_tokens": "OAuth токены", "token": "Токен", "refresh_token": "Рефреш токен", @@ -295,7 +299,14 @@ "use_source": "Новая версия", "use_snapshot": "Старая версия", "keep_as_is": "Оставить, как есть", - "load_theme": "Загрузить тему" + "load_theme": "Загрузить тему", + "help": { + "fe_upgraded": "Движок тем для фронт-энда Pleroma был изменен после обновления.", + "older_version_imported": "Файл, который вы импортировали, был сделан в старой версии фронт-энда.", + "future_version_imported": "Файл, который вы импортировали, был сделан в новой версии фронт-энда.", + "v2_imported": "Файл, который вы импортировали, был сделан под старый фронт-энд. Мы стараемся улучшить совместимость, но все еще возможны несостыковки.", + "upgraded_from_v2": "Фронт-энд Pleroma был изменен. Выбранная тема может выглядеть слегка по-другому." + } }, "common": { "color": "Цвет", @@ -330,7 +341,9 @@ "borders": "Границы", "buttons": "Кнопки", "inputs": "Поля ввода", - "faint_text": "Маловажный текст" + "faint_text": "Маловажный текст", + "post": "Сообщения и описание пользователя", + "alert_neutral": "Нейтральный" }, "radii": { "_tab_label": "Скругление" @@ -451,7 +464,19 @@ "virtual_scrolling": "Оптимизировать рендеринг ленты", "hide_wallpaper": "Скрыть обои узла", "accent": "Акцент", - "upload_a_photo": "Загрузить фото" + "upload_a_photo": "Загрузить фото", + "notification_mutes": "Чтобы не получать уведомления от определённого пользователя, заглушите его.", + "reset_avatar_confirm": "Вы действительно хотите сбросить личный образ?", + "reset_profile_banner": "Сбросить личный баннер", + "reset_profile_background": "Сбросить личные обои", + "reset_avatar": "Сбросить личный образ", + "search_user_to_mute": "Искать, кого вы хотите заглушить", + "search_user_to_block": "Искать, кого вы хотите заблокировать", + "pad_emoji": "Выделять эмодзи пробелами при добавлении из панели", + "avatar_size_instruction": "Желательный наименьший размер личного образа 150 на 150 пикселей.", + "enable_web_push_notifications": "Включить web push-уведомления", + "notification_blocks": "Блокировка пользователя выключает все уведомления от него, а также отписывает вас от него.", + "notification_setting_hide_notification_contents": "Скрыть отправителя и содержимое push-уведомлений" }, "timeline": { "collapse": "Свернуть", @@ -465,7 +490,7 @@ "error": "Ошибка при обновлении ленты: {0}" }, "status": { - "bookmark": "В закладки", + "bookmark": "Добавить в закладки", "unbookmark": "Удалить из закладок", "status_deleted": "Пост удален", "reply_to": "Ответ", @@ -473,7 +498,11 @@ "favorites": "Понравилось", "unmute_conversation": "Прекратить игнорировать разговор", "mute_conversation": "Игнорировать разговор", - "thread_muted": "Разговор игнорируется" + "thread_muted": "Разговор игнорируется", + "external_source": "Перейти к источнику", + "delete_confirm": "Вы действительно хотите удалить данный статус?", + "delete": "Удалить", + "copy_link": "Скопировать ссылку" }, "user_card": { "block": "Заблокировать", @@ -515,7 +544,8 @@ "media": "С вложениями", "mention": "Упомянуть", "show_repeats": "Показывать повторы", - "hide_repeats": "Скрыть повторы" + "hide_repeats": "Скрыть повторы", + "report": "Пожаловаться" }, "user_profile": { "timeline_title": "Лента пользователя" @@ -584,7 +614,9 @@ "title": "Особенности", "gopher": "Gopher", "who_to_follow": "Предложения кого читать", - "pleroma_chat_messages": "Pleroma Чат" + "pleroma_chat_messages": "Pleroma Чат", + "upload_limit": "Наибольший размер загружаемого файла", + "scope_options": "Настраиваемая видимость статусов" }, "tool_tip": { "accept_follow_request": "Принять запрос на чтение", @@ -673,6 +705,7 @@ "you": "Вы:" }, "remote_user_resolver": { - "error": "Не найдено." + "error": "Не найдено.", + "searching_for": "Ищем" } } diff --git a/src/i18n/uk.json b/src/i18n/uk.json @@ -17,7 +17,11 @@ "more": "Більше", "submit": "Відправити", "apply": "Застосувати", - "peek": "Глянути" + "peek": "Глянути", + "role": { + "moderator": "Модератор", + "admin": "Адміністратор" + } }, "finder": { "error_fetching_user": "Користувача не знайдено", @@ -25,11 +29,11 @@ }, "features_panel": { "gopher": "Gopher", - "pleroma_chat_messages": "Локальні балачки", + "pleroma_chat_messages": "Чати", "chat": "Міні-чат", "who_to_follow": "Кого відстежувати", "title": "Особливості", - "scope_options": "Параметри осягу", + "scope_options": "Параметри обсягу", "media_proxy": "Посередник медіа-даних", "text_limit": "Ліміт символів", "upload_limit": "Обмеження завантажень" @@ -39,9 +43,9 @@ "export": "Експорт" }, "domain_mute_card": { - "unmute_progress": "Вимикаю…", + "unmute_progress": "Вмикаю…", "unmute": "Вимкнути заглушення", - "mute_progress": "Вмикаю…", + "mute_progress": "Вимикаю…", "mute": "Ігнорувати" }, "shoutbox": { @@ -51,13 +55,13 @@ "staff": "Адміністрація", "mrf": { "simple": { - "media_nsfw_desc": "Даний інстанс примусово позначає медіа в наступних інстансах як NSFW:", + "media_nsfw_desc": "Даний інстанс примусово позначає медіа в наступних інстансах як дратівливий:", "media_nsfw": "Примусове визначення медіа як дратівливого", "media_removal_desc": "Поточний інстанс видаляє медіа з дописів на перелічених інстансах:", "media_removal": "Видалення медіа", - "ftl_removal_desc": "Цей інстанс видаляє перелічені інстанси з \"Усієї відомої мережі\":", - "ftl_removal": "Видалення з \"Усієї відомої мережі\"", - "quarantine_desc": "Поточний інстанс буде надсилати тільки публічні дописи наступним інстансам:", + "ftl_removal_desc": "Цей інстанс видаляє перелічені інстанси з Федеративної стрічки:", + "ftl_removal": "Видалення зі стрічки Федеративної мережі", + "quarantine_desc": "Поточний інстанс надсилатиме тільки публічні дописи наступним інстансам:", "quarantine": "Карантин", "reject_desc": "Поточний інстанс не прийматиме повідомлення з перелічених інстансів:", "accept": "Прийняти", @@ -66,7 +70,7 @@ "simple_policies": "Правила поточного інстансу" }, "mrf_policies_desc": "Правила MRF розповсюджуються на даний інстанс. Наступні правила активні:", - "mrf_policies": "Активні правила MRF (модуль переписування повідомлень)", + "mrf_policies": "Активувати правила MRF (модуль переписування повідомлень)", "keyword": { "is_replaced_by": "→", "replace": "Замінити", @@ -135,7 +139,7 @@ "error": "Помилка при оновленні сповіщень: {0}" }, "nav": { - "chats": "Локальні балачки", + "chats": "Чати", "timelines": "Стрічки", "twkn": "Уся відома мережа", "about": "Інформація", @@ -546,7 +550,8 @@ "disabled": "Вимкнено", "selectedMenu": "Вибраний пункт меню", "tabs": "Вкладки", - "pressed": "Натиснуто" + "pressed": "Натиснуто", + "wallpaper": "Шпалери" }, "common_colors": { "rgbo": "Піктограми, акценти, значки", @@ -602,7 +607,8 @@ "frontend_version": "Версія фронтенду", "backend_version": "Версія бекенду", "title": "Версія" - } + }, + "hide_wallpaper": "Сховати шпалери екземпляру" }, "selectable_list": { "select_all": "Вибрати все" diff --git a/src/i18n/zh.json b/src/i18n/zh.json @@ -39,7 +39,11 @@ "close": "关闭", "retry": "重试", "error_retry": "请重试", - "loading": "载入中…" + "loading": "载入中…", + "role": { + "moderator": "监察员", + "admin": "管理员" + } }, "image_cropper": { "crop_picture": "裁剪图片", @@ -120,7 +124,9 @@ "expiry": "投票期限", "expires_in": "投票于 {0} 后结束", "expired": "投票 {0} 前已结束", - "not_enough_options": "投票的选项太少" + "not_enough_options": "投票的选项太少", + "votes_count": "{count} 票 | {count} 票", + "people_voted_count": "{count} 人已投票 | {count} 人已投票" }, "stickers": { "add_sticker": "添加贴纸" @@ -183,7 +189,9 @@ "password_required": "不能留空", "password_confirmation_required": "不能留空", "password_confirmation_match": "密码不一致" - } + }, + "reason_placeholder": "此实例的注册需要手动批准。\n请让管理员知道您为什么想要注册。", + "reason": "注册理由" }, "selectable_list": { "select_all": "选择全部" @@ -552,7 +560,8 @@ "mute_import": "隐藏名单导入", "mute_export_button": "导出你的隐藏名单到一个 csv 文件", "mute_export": "隐藏名单导出", - "hide_wallpaper": "隐藏实例壁纸" + "hide_wallpaper": "隐藏实例壁纸", + "setting_changed": "与默认设置不同" }, "time": { "day": "{0} 天", @@ -683,7 +692,8 @@ "show_repeats": "显示转发", "hide_repeats": "隐藏转发", "message": "消息", - "mention": "提及" + "mention": "提及", + "bot": "机器人" }, "user_profile": { "timeline_title": "用户时间线", diff --git a/src/i18n/zh_Hant.json b/src/i18n/zh_Hant.json @@ -25,7 +25,7 @@ "add_poll": "增加投票" }, "notifications": { - "reacted_with": "和 {0} 互動過", + "reacted_with": "作出了 {0} 的反應", "migrated_to": "遷移到", "no_more_notifications": "沒有更多的通知", "repeated_you": "轉發了你的發文", @@ -54,7 +54,7 @@ "mentions": "提及", "friend_requests": "關注請求", "back": "後退", - "administration": "管理", + "administration": "管理員", "about": "關於" }, "media_modal": { @@ -216,7 +216,8 @@ "incoming": "收到", "outgoing": "發出", "border": "邊框" - } + }, + "wallpaper": "桌布" }, "preview": { "header_faint": "這很正常", @@ -412,7 +413,7 @@ "hide_follows_description": "不要顯示我所關注的人", "hide_followers_description": "不要顯示關注我的人", "hide_follows_count_description": "不顯示關注數", - "nsfw_clickthrough": "將敏感附件隱藏,點擊才能打開", + "nsfw_clickthrough": "將敏感附件和鏈接隱藏,點擊才能打開", "valid_until": "有效期至", "panelRadius": "面板", "pause_on_unfocused": "在離開頁面時暫停時間線推送", @@ -526,7 +527,8 @@ "mute_import": "靜音導入", "mute_import_error": "導入靜音時出錯", "mute_export_button": "將靜音導出到csv文件", - "mute_export": "靜音導出" + "mute_export": "靜音導出", + "hide_wallpaper": "隱藏實例桌布" }, "chats": { "more": "更多", @@ -571,16 +573,20 @@ "thread_muted_and_words": ",有这些字:", "hide_full_subject": "隱藏完整標題", "show_content": "顯示內容", - "hide_content": "隱藏內容" + "hide_content": "隱藏內容", + "status_deleted": "該帖已被刪除", + "expand": "展开", + "external_source": "外部來源", + "nsfw": "工作不安全" }, "time": { - "hours": "{0} 小時", + "hours": "{0} 時", "days_short": "{0}天", "day_short": "{0}天", "days": "{0} 天", - "hour": "{0} 小时", - "hour_short": "{0}h", - "hours_short": "{0}h", + "hour": "{0} 時", + "hour_short": "{0}時", + "hours_short": "{0}時", "years_short": "{0} y", "now": "剛剛", "day": "{0} 天", @@ -654,7 +660,8 @@ "reload": "重新載入", "up_to_date": "已是最新", "no_more_statuses": "没有更多發文", - "no_statuses": "没有發文" + "no_statuses": "没有發文", + "error": "取得時間線時發生錯誤:{0}" }, "interactions": { "load_older": "載入更早的互動", @@ -745,7 +752,11 @@ "unmute": "取消靜音", "unmute_progress": "取消靜音中…", "hide_repeats": "隱藏轉發", - "show_repeats": "顯示轉發" + "show_repeats": "顯示轉發", + "roles": { + "moderator": "主持人", + "admin": "管理員" + } }, "user_profile": { "timeline_title": "用戶時間線", @@ -787,7 +798,8 @@ "error": { "base": "上傳失敗。", "file_too_big": "文件太大[{filesize} {filesizeunit} / {allowedsize} {allowedsizeunit}]", - "default": "稍後再試" + "default": "稍後再試", + "message": "上傳錯誤:{0}" } }, "search": { diff --git a/src/main.js b/src/main.js @@ -28,7 +28,6 @@ import pushNotifications from './lib/push_notifications_plugin.js' import messages from './i18n/messages.js' -import VueChatScroll from 'vue-chat-scroll' import VueClickOutside from 'v-click-outside' import PortalVue from 'portal-vue' import VBodyScrollLock from './directives/body_scroll_lock' @@ -42,7 +41,6 @@ const currentLocale = (window.navigator.language || 'en').split('-')[0] Vue.use(Vuex) Vue.use(VueRouter) Vue.use(VueI18n) -Vue.use(VueChatScroll) Vue.use(VueClickOutside) Vue.use(PortalVue) Vue.use(VBodyScrollLock) diff --git a/src/modules/chat.js b/src/modules/chat.js @@ -18,6 +18,7 @@ const chat = { actions: { initializeChat (store, socket) { const channel = socket.channel('chat:public') + channel.on('new_msg', (msg) => { store.commit('addMessage', msg) }) diff --git a/src/modules/chats.js b/src/modules/chats.js @@ -115,6 +115,9 @@ const chats = { }, handleMessageError ({ commit }, value) { commit('handleMessageError', { commit, ...value }) + }, + cullOlderMessages ({ commit }, chatId) { + commit('cullOlderMessages', chatId) } }, mutations: { @@ -227,6 +230,9 @@ const chats = { handleMessageError (state, { chatId, fakeId, isRetry }) { const chatMessageService = state.openedChatMessageServices[chatId] chatService.handleMessageError(chatMessageService, fakeId, isRetry) + }, + cullOlderMessages (state, chatId) { + chatService.cullOlderMessages(state.openedChatMessageServices[chatId]) } } } diff --git a/src/modules/config.js b/src/modules/config.js @@ -67,7 +67,8 @@ export const defaultState = { greentext: undefined, // instance default hidePostStats: undefined, // instance default hideUserStats: undefined, // instance default - virtualScrolling: undefined // instance default + virtualScrolling: undefined, // instance default + sensitiveByDefault: undefined // instance default } // caching the instance default properties @@ -76,18 +77,22 @@ export const instanceDefaultProperties = Object.entries(defaultState) .map(([key, value]) => key) const config = { - state: defaultState, + state: { ...defaultState }, getters: { - mergedConfig (state, getters, rootState, rootGetters) { + defaultConfig (state, getters, rootState, rootGetters) { const { instance } = rootState return { - ...state, - ...instanceDefaultProperties - .map(key => [key, state[key] === undefined - ? instance[key] - : state[key] - ]) - .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) + ...defaultState, + ...Object.fromEntries( + instanceDefaultProperties.map(key => [key, instance[key]]) + ) + } + }, + mergedConfig (state, getters, rootState, rootGetters) { + const { defaultConfig } = rootGetters + return { + ...defaultConfig, + ...state } } }, diff --git a/src/modules/instance.js b/src/modules/instance.js @@ -43,6 +43,7 @@ const defaultState = { subjectLineBehavior: 'email', theme: 'pleroma-dark', virtualScrolling: true, + sensitiveByDefault: false, // Nasty stuff customEmoji: [], diff --git a/src/modules/statuses.js b/src/modules/statuses.js @@ -13,7 +13,11 @@ import { omitBy } from 'lodash' import { set } from 'vue' -import { isStatusNotification, maybeShowNotification } from '../services/notification_utils/notification_utils.js' +import { + isStatusNotification, + isValidNotification, + maybeShowNotification +} from '../services/notification_utils/notification_utils.js' import apiService from '../services/api/api.service.js' const emptyTl = (userId = 0) => ({ @@ -310,8 +314,24 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } } +const updateNotificationsMinMaxId = (state, notification) => { + state.notifications.maxId = notification.id > state.notifications.maxId + ? notification.id + : state.notifications.maxId + state.notifications.minId = notification.id < state.notifications.minId + ? notification.id + : state.notifications.minId +} + const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes, rootGetters, newNotificationSideEffects }) => { each(notifications, (notification) => { + // If invalid notification, update ids but don't add it to store + if (!isValidNotification(notification)) { + console.error('Invalid notification:', notification) + updateNotificationsMinMaxId(state, notification) + return + } + if (isStatusNotification(notification.type)) { notification.action = addStatusToGlobalStorage(state, notification.action).item notification.status = notification.status && addStatusToGlobalStorage(state, notification.status).item @@ -323,12 +343,7 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot // Only add a new notification if we don't have one for the same action if (!state.notifications.idStore.hasOwnProperty(notification.id)) { - state.notifications.maxId = notification.id > state.notifications.maxId - ? notification.id - : state.notifications.maxId - state.notifications.minId = notification.id < state.notifications.minId - ? notification.id - : state.notifications.minId + updateNotificationsMinMaxId(state, notification) state.notifications.data.push(notification) state.notifications.idStore[notification.id] = notification diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js @@ -48,6 +48,22 @@ const deleteMessage = (storage, messageId) => { } } +const cullOlderMessages = (storage) => { + const maxIndex = storage.messages.length + const minIndex = maxIndex - 50 + if (maxIndex <= 50) return + + storage.messages = _.sortBy(storage.messages, ['id']) + storage.minId = storage.messages[minIndex].id + for (const message of storage.messages) { + if (message.id < storage.minId) { + delete storage.idIndex[message.id] + delete storage.idempotencyKeyIndex[message.idempotency_key] + } + } + storage.messages = storage.messages.slice(minIndex, maxIndex) +} + const handleMessageError = (storage, fakeId, isRetry) => { if (!storage) { return } const fakeMessage = storage.idIndex[fakeId] @@ -201,6 +217,7 @@ const ChatService = { empty, getView, deleteMessage, + cullOlderMessages, resetNewMessageCount, clear, handleMessageError diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js @@ -188,7 +188,12 @@ export const parseUser = (data) => { output.follow_request_count = data.pleroma.follow_request_count output.tags = data.pleroma.tags - output.deactivated = data.pleroma.deactivated + + // deactivated was changed to is_active in Pleroma 2.3.0 + // so check if is_active is present + output.deactivated = typeof data.pleroma.is_active !== 'undefined' + ? !data.pleroma.is_active // new backend + : data.pleroma.deactivated // old backend output.notification_settings = data.pleroma.notification_settings output.unread_chat_count = data.pleroma.unread_chat_count @@ -198,7 +203,8 @@ export const parseUser = (data) => { output.rights = output.rights || {} output.notification_settings = output.notification_settings || {} - // Convert punycode to unicode + // Convert punycode to unicode for UI + output.screen_name_ui = output.screen_name if (output.screen_name.includes('@')) { const parts = output.screen_name.split('@') let unicodeDomain = punycode.toUnicode(parts[1]) @@ -206,7 +212,7 @@ export const parseUser = (data) => { // Add some identifier so users can potentially spot spoofing attempts: // lain.com and xn--lin-6cd.com would appear identical otherwise. unicodeDomain = '🌏' + unicodeDomain - output.screen_name = [parts[0], unicodeDomain].join('@') + output.screen_name_ui = [parts[0], unicodeDomain].join('@') } } diff --git a/src/services/locale/locale.service.js b/src/services/locale/locale.service.js @@ -0,0 +1,12 @@ +const specialLanguageCodes = { + 'ja_easy': 'ja', + 'zh_Hant': 'zh-HANT' +} + +const internalToBrowserLocale = code => specialLanguageCodes[code] || code + +const localeService = { + internalToBrowserLocale +} + +export default localeService diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js @@ -22,6 +22,13 @@ const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reactio export const isStatusNotification = (type) => includes(statusNotifications, type) +export const isValidNotification = (notification) => { + if (isStatusNotification(notification.type) && !notification.status) { + return false + } + return true +} + const sortById = (a, b) => { const seqA = Number(a.id) const seqB = Number(b.id) diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js @@ -242,9 +242,18 @@ export const generateShadows = (input, colors) => { panelHeader: 'panel', input: 'input' } - const inputShadows = input.shadows && !input.themeEngineVersion - ? shadows2to3(input.shadows, input.opacity) - : input.shadows || {} + + const cleanInputShadows = Object.fromEntries( + Object.entries(input.shadows || {}) + .map(([name, shadowSlot]) => [ + name, + // defaulting color to black to avoid potential problems + shadowSlot.map(shadowDef => ({ color: '#000000', ...shadowDef })) + ]) + ) + const inputShadows = cleanInputShadows && !input.themeEngineVersion + ? shadows2to3(cleanInputShadows, input.opacity) + : cleanInputShadows || {} const shadows = Object.entries({ ...DEFAULT_SHADOWS, ...inputShadows diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js @@ -31,13 +31,15 @@ const testGetters = { const localUser = { id: 100, is_local: true, - screen_name: 'testUser' + screen_name: 'testUser', + screen_name_ui: 'testUser' } const extUser = { id: 100, is_local: false, - screen_name: 'testUser@test.instance' + screen_name: 'testUser@test.instance', + screen_name_ui: 'testUser@test.instance' } const externalProfileStore = new Vuex.Store({ diff --git a/test/unit/specs/services/chat_service/chat_service.spec.js b/test/unit/specs/services/chat_service/chat_service.spec.js @@ -88,4 +88,21 @@ describe('chatService', () => { expect(view.map(i => i.type)).to.eql(['date', 'message', 'message', 'date', 'message']) }) }) + + describe('.cullOlderMessages', () => { + it('keeps 50 newest messages and idIndex matches', () => { + const chat = chatService.empty() + + for (let i = 100; i > 0; i--) { + // Use decimal values with toFixed to hack together constant length predictable strings + chatService.add(chat, { messages: [{ ...message1, id: 'a' + (i / 1000).toFixed(3), idempotency_key: i }] }) + } + chatService.cullOlderMessages(chat) + expect(chat.messages.length).to.eql(50) + expect(chat.messages[0].id).to.eql('a0.051') + expect(chat.minId).to.eql('a0.051') + expect(chat.messages[49].id).to.eql('a0.100') + expect(Object.keys(chat.idIndex).length).to.eql(50) + }) + }) }) diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -315,7 +315,7 @@ describe('API Entities normalizer', () => { it('converts IDN to unicode and marks it as internatonal', () => { const user = makeMockUserMasto({ acct: 'lain@xn--lin-6cd.com' }) - expect(parseUser(user)).to.have.property('screen_name').that.equal('lain@🌏lаin.com') + expect(parseUser(user)).to.have.property('screen_name_ui').that.equal('lain@🌏lаin.com') }) }) diff --git a/yarn.lock b/yarn.lock @@ -7842,9 +7842,10 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" -shelljs@^0.7.4: - version "0.7.8" - resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.8.tgz#decbcf874b0d1e5fb72e14b164a9683048e9acb3" +shelljs@^0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" + integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== dependencies: glob "^7.0.0" interpret "^1.0.0" @@ -8922,10 +8923,6 @@ void-elements@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" -vue-chat-scroll@^1.2.1: - version "1.3.5" - resolved "https://registry.yarnpkg.com/vue-chat-scroll/-/vue-chat-scroll-1.3.5.tgz#a5ee5bae5058f614818a96eac5ee3be4394a2f68" - vue-eslint-parser@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-5.0.0.tgz#00f4e4da94ec974b821a26ff0ed0f7a78402b8a1"