diff --git a/.babelrc b/.babelrc index 3c732dd1bc7589c28ca0531e5cb05cf4b1968cd2..945211470bf26243a855bf15c88f80f0d75610fe 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,5 @@ { - "presets": ["@babel/preset-env"], - "plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"], + "presets": ["@babel/preset-env", "@vue/babel-preset-jsx"], + "plugins": ["@babel/plugin-transform-runtime", "lodash"], "comments": false } diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000000000000000000000000000000000..0b198a4703ec10d9f70facf12e6198a1272a1a99 --- /dev/null +++ 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 index 588348a0fe4e495c9f112228482250f07ae06000..ae54025a39b9cb96e3596b718fb51bcce58c5ff5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,88 @@ 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] +## Unreleased +### Fixed +- AdminFE button no longer scrolls page to top when clicked +- Pinned statuses no longer appear at bottom of user timeline (still appear as part of the timeline when fetched deep enough) +- Fixed many many bugs related to new mentions, including spacing and alignment issues +- Links in profile bios now properly open in new tabs +- Inline images now respect their intended width/height attributes +- Links with `&` in them work properly now +- Interaction list popovers now properly emojify names +- Completely hidden posts still had 1px border +- Attachments are ALWAYS in same order as user uploaded, no more "videos first" +- Attachment description is prefilled with backend-provided default when uploading +- Proper visual feedback that next image is loading when browsing + +### Changed +- (You)s are optional (opt-in) now, bolding your nickname is also optional (opt-out) +- User highlight background now also covers the `@` +- Reverted back to textual `@`, svg version is opt-in. +- Settings window has been throughly rearranged to make make more sense and make navication settings easier. +- Uploaded attachments are uniform with displayed attachments +- Flash is watchable in media-modal (takes up nearly full screen though due to sizing issues) +- Notifications about likes/repeats/emoji reacts are now minimized so they always take up same amount of space irrelevant to size of post. + +### Added +- Options to show domains in mentions +- Option to show user avatars in mention links (opt-in) +- Option to disable the tooltip for mentions +- Option to completely hide muted threads +- Ability to open videos in modal even if you disabled that feature, via an icon button +- New button on attachment that indicates that attachment has a description and shows a bar filled with description +- Attachments are truncated just like post contents +- Media modal now also displays description and counter position in gallery (i.e. 1/5) +- Ability to rearrange order of attachments when uploading + +## [2.4.2] - 2022-01-09 +### Added +- Added Apply and Reset buttons to the bottom of theme tab to minimize UI travel +- Implemented user option to always show floating New Post button (normally mobile-only) +- Display reasons for instance specific policies +- Added functionality to cancel follow request + +### Fixed +- Fixed link to external profile not working on user profiles +- Fixed mobile shoutbox display +- Fixed favicon badge not working in Chrome +- Escape html more properly in subject/display name + + +## [2.4.0] - 2021-08-08 +### Added +- Added a quick settings to timeline header for easier access +- Added option to mark posts as sensitive by default +- Added quick filters for notifications +- Implemented user option to change sidebar position to the right side +- Implemented user option to hide floating shout panel +- Implemented "edit profile" button if viewing own profile which opens profile settings + +### Fixed +- Fixed follow request count showing in the wrong location in mobile view + + +## [2.3.0] - 2021-03-01 ### 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 +- Changed the "Timelines" link in side panel to toggle show all timeline options inside the panel +- Renamed "Timeline" to "Home Timeline" to be more clear +- 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 @@ -16,10 +94,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - Follows/Followers tabs on user profiles now display the content properly. - Handle punycode in screen names +- Fixed local dev mode having non-functional websockets in some cases +- Show notices for websocket events (errors, abnormal closures, reconnections) +- 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 -- Language picker now uses native language names ## [2.2.2] - 2020-12-22 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d7c217cef1c2f37a2b347ff919cc53ded2e49408..f666a4ef02461c2c68051a3322dda18da096f28d 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -3,6 +3,7 @@ Contributors of this project. - Constance Variable (lambadalambda@social.heldscal.la): Code - Coco Snuss (cocosnuss@social.heldscal.la): Code - wakarimasen (wakarimasen@shitposter.club): NSFW hiding image +- eris (eris@disqordia.space): Code - dtluna (dtluna@social.heldscal.la): Code - sonyam (sonyam@social.heldscal.la): Background images - hakui (hakui@freezepeach.xyz): CSS and styling diff --git a/build/dev-server.js b/build/dev-server.js index 485742147152de18ac97be90f89ceeb197731365..c06192bd1e0ba8359d9fc583337ab53663adb697 100644 --- a/build/dev-server.js +++ b/build/dev-server.js @@ -21,6 +21,7 @@ var compiler = webpack(webpackConfig) var devMiddleware = require('webpack-dev-middleware')(compiler, { publicPath: webpackConfig.output.publicPath, + writeToDisk: true, stats: { colors: true, chunks: false diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index d987eff1034f4c667de7df878b9fc587d270759e..900d824b9877ecc40edbe4ef2d6433f636dc3868 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -3,6 +3,7 @@ var config = require('../config') var utils = require('./utils') var projectRoot = path.resolve(__dirname, '../') var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin') +var CopyPlugin = require('copy-webpack-plugin'); var env = process.env.NODE_ENV // check env & config/index.js to decide weither to enable CSS Sourcemaps for the @@ -93,6 +94,19 @@ module.exports = { new ServiceWorkerWebpackPlugin({ entry: path.join(__dirname, '..', 'src/sw.js'), filename: 'sw-pleroma.js' + }), + // This copies Ruffle's WASM to a directory so that JS side can access it + new CopyPlugin({ + patterns: [ + { + from: "node_modules/ruffle-mirror/*", + to: "static/ruffle", + flatten: true + }, + ], + options: { + concurrency: 100, + }, }) ] } diff --git a/config/index.js b/config/index.js index ccec419654c914ad52070c312a55f24614f48a78..7cb87c3b368cee12f95eb32d0fc03c65707a5997 100644 --- a/config/index.js +++ b/config/index.js @@ -3,6 +3,11 @@ const path = require('path') let settings = {} try { settings = require('./local.json') + if (settings.target && settings.target.endsWith('/')) { + // replacing trailing slash since it can conflict with some apis + // and that's how actual BE reports its url + settings.target = settings.target.replace(/\/$/, '') + } console.log('Using local dev server settings (/config/local.json):') console.log(JSON.stringify(settings, null, 2)) } catch (e) { diff --git a/package.json b/package.json index e11396bf68f1dc7f641699c8ee65baa5278656ff..5134a8b1578b3bd86f295c80e22a08000fac7f44 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,9 @@ "phoenix": "^1.3.0", "portal-vue": "^2.1.4", "punycode.js": "^2.1.0", + "ruffle-mirror": "^2021.4.10", "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", @@ -47,8 +47,8 @@ "@babel/preset-env": "^7.7.6", "@babel/register": "^7.7.4", "@ungap/event-target": "^0.1.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.0.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", + "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", + "@vue/babel-preset-jsx": "^1.2.4", "@vue/test-utils": "^1.0.0-beta.26", "autoprefixer": "^6.4.0", "babel-eslint": "^7.0.0", @@ -58,6 +58,7 @@ "chalk": "^1.1.3", "chromedriver": "^87.0.1", "connect-history-api-fallback": "^1.1.0", + "copy-webpack-plugin": "^6.4.1", "cross-spawn": "^4.0.2", "css-loader": "^0.28.0", "custom-event-polyfill": "^1.0.7", @@ -103,7 +104,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", @@ -112,7 +113,7 @@ "url-loader": "^1.1.2", "vue-loader": "^14.0.0", "vue-style-loader": "^4.0.0", - "webpack": "^4.0.0", + "webpack": "^4.44.0", "webpack-dev-middleware": "^3.6.0", "webpack-hot-middleware": "^2.12.2", "webpack-merge": "^0.14.1" diff --git a/src/App.js b/src/App.js index 1ca029b69aeebeb9b35fbd0de6ad53601d654edc..f5e0b9e9b6223fb40de476e322aae472ebbe41a0 100644 --- a/src/App.js +++ b/src/App.js @@ -4,7 +4,7 @@ import Notifications from './components/notifications/notifications.vue' import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' -import ChatPanel from './components/chat_panel/chat_panel.vue' +import ShoutPanel from './components/shout_panel/shout_panel.vue' import SettingsModal from './components/settings_modal/settings_modal.vue' import MediaModal from './components/media_modal/media_modal.vue' import SideDrawer from './components/side_drawer/side_drawer.vue' @@ -26,7 +26,7 @@ export default { InstanceSpecificPanel, FeaturesPanel, WhoToFollowPanel, - ChatPanel, + ShoutPanel, MediaModal, SideDrawer, MobilePostStatusButton, @@ -65,7 +65,7 @@ export default { } } }, - chat () { return this.$store.state.chat.channel.state === 'joined' }, + shout () { return this.$store.state.shout.channel.state === 'joined' }, suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled }, showInstanceSpecificPanel () { return this.$store.state.instance.showInstanceSpecificPanel && @@ -73,11 +73,17 @@ export default { this.$store.state.instance.instanceSpecificPanelContent }, showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel }, + shoutboxPosition () { + return this.$store.getters.mergedConfig.showNewPostButton || false + }, + hideShoutbox () { + return this.$store.getters.mergedConfig.hideShoutbox + }, isMobileLayout () { return this.$store.state.interface.mobileLayout }, privateMode () { return this.$store.state.instance.private }, sidebarAlign () { return { - 'order': this.$store.state.instance.sidebarRight ? 99 : 0 + 'order': this.$store.getters.mergedConfig.sidebarRight ? 99 : 0 } }, ...mapGetters(['mergedConfig']) diff --git a/src/App.scss b/src/App.scss index 8b91f3dea12a8819785b45fa25e25fab6c413d6d..bc027f4fca3849b3dd83ef2bcc485b969e0e407c 100644 --- a/src/App.scss +++ b/src/App.scss @@ -88,6 +88,10 @@ a { font-family: sans-serif; font-family: var(--interfaceFont, sans-serif); + &.-sublime { + background: transparent; + } + i[class*=icon-], .svg-inline--fa { color: $fallback--text; @@ -187,7 +191,7 @@ a { } } -input, textarea, .select, .input { +input, textarea, .input { &.unstyled { border-radius: 0; @@ -217,47 +221,11 @@ input, textarea, .select, .input { hyphens: none; padding: 8px .5em; - &.select { - padding: 0; - } - - &:disabled, &[disabled=disabled] { + &:disabled, &[disabled=disabled], &.disabled { cursor: not-allowed; opacity: 0.5; } - .select-down-icon { - position: absolute; - top: 0; - bottom: 0; - right: 5px; - height: 100%; - color: $fallback--text; - color: var(--inputText, $fallback--text); - line-height: 28px; - z-index: 0; - pointer-events: none; - } - - select { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background: transparent; - border: none; - color: $fallback--text; - color: var(--inputText, --text, $fallback--text); - margin: 0; - padding: 0 2em 0 .2em; - font-family: sans-serif; - font-family: var(--inputFont, sans-serif); - font-size: 14px; - width: 100%; - z-index: 1; - height: 28px; - line-height: 16px; - } - &[type=range] { background: none; border: none; @@ -547,9 +515,21 @@ main-router { border-radius: var(--panelRadius, $fallback--panelRadius); } -.panel-footer { +/* TODO Should remove timeline-footer from here when we refactor panels into + * separate component and utilize slots + */ +.panel-footer, .timeline-footer { + display: flex; border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + flex: none; + padding: 0.6em 0.6em; + text-align: left; + line-height: 28px; + align-items: baseline; + border-width: 1px 0 0 0; + border-style: solid; + border-color: var(--border, $fallback--border); .faint { color: $fallback--faint; @@ -586,6 +566,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 { @@ -705,6 +686,15 @@ nav { color: var(--alertWarningPanelText, $fallback--text); } } + + &.success { + background-color: var(--alertSuccess, $fallback--alertWarning); + color: var(--alertSuccessText, $fallback--text); + + .panel-heading & { + color: var(--alertSuccessPanelText, $fallback--text); + } + } } .faint { @@ -808,13 +798,6 @@ nav { } } -.select-multiple { - display: flex; - .option-list { - margin: 0; - padding-left: .5em; - } -} .setting-list, .option-list{ list-style-type: none; @@ -861,16 +844,10 @@ nav { } .new-status-notification { - position:relative; - margin-top: -1px; + position: relative; font-size: 1.1em; - border-width: 1px 0 0 0; - border-style: solid; - border-color: var(--border, $fallback--border); - padding: 10px; z-index: 1; - background-color: $fallback--fg; - background-color: var(--panel, $fallback--fg); + flex: 1; } .chat-layout { @@ -878,6 +855,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/App.vue b/src/App.vue index 1a1667788d1cba17870fb0167ca7a6cffd5ff401..eb65b548d8da27e22005d6af61084d0b61c98b73 100644 --- a/src/App.vue +++ b/src/App.vue @@ -49,10 +49,11 @@ </div> <media-modal /> </div> - <chat-panel - v-if="currentUser && chat" + <shout-panel + v-if="currentUser && shout && !hideShoutbox" :floating="true" - class="floating-chat mobile-hidden" + class="floating-shout mobile-hidden" + :class="{ 'left': shoutboxPosition }" /> <MobilePostStatusButton /> <UserReportingModal /> diff --git a/src/boot/after_store.js b/src/boot/after_store.js index b472fcf64e4ed887fec4d77f66db21350d8a95d6..cc0c7c5e2fc4448f2fdf907751d5e403deae27bb 100644 --- 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 }) @@ -239,7 +240,7 @@ const getNodeInfo = async ({ store }) => { store.dispatch('setInstanceOption', { name: 'registrationOpen', value: data.openRegistrations }) store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) store.dispatch('setInstanceOption', { name: 'safeDM', value: features.includes('safe_dm_mentions') }) - store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') }) + store.dispatch('setInstanceOption', { name: 'shoutAvailable', value: features.includes('chat') }) store.dispatch('setInstanceOption', { name: 'pleromaChatMessagesAvailable', value: features.includes('pleroma_chat_messages') }) store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') }) diff --git a/src/boot/routes.js b/src/boot/routes.js index b5d3c6315c27ae3c3355bc8e91637e4e25ea7819..1bc1f9f7da9e192d65b5b962f757f947ec3d2689 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -16,7 +16,7 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue' import OAuthCallback from 'components/oauth_callback/oauth_callback.vue' import Notifications from 'components/notifications/notifications.vue' import AuthForm from 'components/auth_form/auth_form.js' -import ChatPanel from 'components/chat_panel/chat_panel.vue' +import ShoutPanel from 'components/shout_panel/shout_panel.vue' import WhoToFollow from 'components/who_to_follow/who_to_follow.vue' import About from 'components/about/about.vue' import RemoteUserResolver from 'components/remote_user_resolver/remote_user_resolver.vue' @@ -64,7 +64,7 @@ export default (store) => { { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'notifications', path: '/:username/notifications', component: Notifications, beforeEnter: validateAuthenticatedRoute }, { name: 'login', path: '/login', component: AuthForm }, - { name: 'chat-panel', path: '/chat-panel', component: ChatPanel, props: () => ({ floating: false }) }, + { name: 'shout-panel', path: '/shout-panel', component: ShoutPanel, props: () => ({ floating: false }) }, { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, { name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) }, { name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute }, diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index ab5d1d2982e8df4ad84a132927308b7a98dad9d9..1e31151ca609e27826d42f6fff771174109dc457 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -6,10 +6,7 @@ :bound-to="{ x: 'container' }" remove-padding > - <div - slot="content" - class="account-tools-popover" - > + <template v-slot:content> <div class="dropdown-menu"> <template v-if="relationship.following"> <button @@ -59,16 +56,15 @@ {{ $t('user_card.message') }} </button> </div> - </div> - <div - slot="trigger" - class="ellipsis-button" - > - <FAIcon - class="icon" - icon="ellipsis-v" - /> - </div> + </template> + <template v-slot:trigger> + <button class="button-unstyled ellipsis-button"> + <FAIcon + class="icon" + icon="ellipsis-v" + /> + </button> + </template> </Popover> </div> </template> @@ -83,7 +79,6 @@ } .ellipsis-button { - cursor: pointer; width: 2.5em; margin: -0.5em 0; padding: 0.5em 0; diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 5f5779a0694ffb402cddd527d6f2d22fbc62a2d5..d62a4adc21475816172d797b2a9cebd01a412f01 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -1,4 +1,5 @@ import StillImage from '../still-image/still-image.vue' +import Flash from '../flash/flash.vue' import VideoAttachment from '../video_attachment/video_attachment.vue' import nsfwImage from '../../assets/nsfw.png' import fileTypeService from '../../services/file_type/file_type.service.js' @@ -10,7 +11,12 @@ import { faImage, faVideo, faPlayCircle, - faTimes + faTimes, + faStop, + faSearchPlus, + faTrashAlt, + faPencilAlt, + faAlignRight } from '@fortawesome/free-solid-svg-icons' library.add( @@ -19,36 +25,64 @@ library.add( faImage, faVideo, faPlayCircle, - faTimes + faTimes, + faStop, + faSearchPlus, + faTrashAlt, + faPencilAlt, + faAlignRight ) const Attachment = { props: [ 'attachment', + 'description', + 'hideDescription', 'nsfw', 'size', - 'allowPlay', 'setMedia', - 'naturalSizeLoad' + 'remove', + 'shiftUp', + 'shiftDn', + 'edit' ], data () { return { + localDescription: this.description || this.attachment.description, nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage, hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw, preloadImage: this.$store.getters.mergedConfig.preloadImage, loading: false, img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), modalOpen: false, - showHidden: false + showHidden: false, + flashLoaded: false, + showDescription: false } }, components: { + Flash, StillImage, VideoAttachment }, computed: { + classNames () { + return [ + { + '-loading': this.loading, + '-nsfw-placeholder': this.hidden, + '-editable': this.edit !== undefined + }, + '-type-' + this.type, + this.size && '-size-' + this.size, + `-${this.useContainFit ? 'contain' : 'cover'}-fit` + ] + }, usePlaceholder () { - return this.size === 'hide' || this.type === 'unknown' + return this.size === 'hide' + }, + useContainFit () { + return this.$store.getters.mergedConfig.useContainFit }, placeholderName () { if (this.attachment.description === '' || !this.attachment.description) { @@ -72,24 +106,33 @@ const Attachment = { return this.nsfw && this.hideNsfwLocal && !this.showHidden }, isEmpty () { - return (this.type === 'html' && !this.attachment.oembed) || this.type === 'unknown' - }, - isSmall () { - return this.size === 'small' - }, - fullwidth () { - if (this.size === 'hide') return false - return this.type === 'html' || this.type === 'audio' || this.type === 'unknown' + return (this.type === 'html' && !this.attachment.oembed) }, useModal () { - const modalTypes = this.size === 'hide' ? ['image', 'video', 'audio'] - : this.mergedConfig.playVideosInModal - ? ['image', 'video'] - : ['image'] + let modalTypes = [] + switch (this.size) { + case 'hide': + case 'small': + modalTypes = ['image', 'video', 'audio', 'flash'] + break + default: + modalTypes = this.mergedConfig.playVideosInModal + ? ['image', 'video', 'flash'] + : ['image'] + break + } return modalTypes.includes(this.type) }, + videoTag () { + return this.useModal ? 'button' : 'span' + }, ...mapGetters(['mergedConfig']) }, + watch: { + localDescription (newVal) { + this.onEdit(newVal) + } + }, methods: { linkClicked ({ target }) { if (target.tagName === 'A') { @@ -98,12 +141,37 @@ const Attachment = { }, openModal (event) { if (this.useModal) { - event.stopPropagation() - event.preventDefault() - this.setMedia() - this.$store.dispatch('setCurrent', this.attachment) + this.$emit('setMedia') + this.$store.dispatch('setCurrentMedia', this.attachment) + } else if (this.type === 'unknown') { + window.open(this.attachment.url) } }, + openModalForce (event) { + this.$emit('setMedia') + this.$store.dispatch('setCurrentMedia', this.attachment) + }, + onEdit (event) { + this.edit && this.edit(this.attachment, event) + }, + onRemove () { + this.remove && this.remove(this.attachment) + }, + onShiftUp () { + this.shiftUp && this.shiftUp(this.attachment) + }, + onShiftDn () { + this.shiftDn && this.shiftDn(this.attachment) + }, + stopFlash () { + this.$refs.flash.closePlayer() + }, + setFlashLoaded (event) { + this.flashLoaded = event + }, + toggleDescription () { + this.showDescription = !this.showDescription + }, toggleHidden (event) { if ( (this.mergedConfig.useOneClickNsfw && !this.showHidden) && @@ -130,7 +198,7 @@ const Attachment = { onImageLoad (image) { const width = image.naturalWidth const height = image.naturalHeight - this.naturalSizeLoad && this.naturalSizeLoad({ width, height }) + this.$emit('naturalSizeLoad', { id: this.attachment.id, width, height }) } } } diff --git a/src/components/attachment/attachment.scss b/src/components/attachment/attachment.scss new file mode 100644 index 0000000000000000000000000000000000000000..dfda15bf8d028d0729def47bb97dedaae856662f --- /dev/null +++ b/src/components/attachment/attachment.scss @@ -0,0 +1,268 @@ +@import '../../_variables.scss'; + +.Attachment { + display: inline-flex; + flex-direction: column; + position: relative; + align-self: flex-start; + line-height: 0; + height: 100%; + border-style: solid; + border-width: 1px; + border-radius: $fallback--attachmentRadius; + border-radius: var(--attachmentRadius, $fallback--attachmentRadius); + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + + .attachment-wrapper { + flex: 1 1 auto; + height: 100%; + position: relative; + overflow: hidden; + } + + .description-container { + flex: 0 1 0; + display: flex; + padding-top: 0.5em; + z-index: 1; + + p { + flex: 1; + text-align: center; + line-height: 1.5; + padding: 0.5em; + margin: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + &.-static { + position: absolute; + left: 0; + right: 0; + bottom: 0; + padding-top: 0; + background: var(--popover); + box-shadow: var(--popupShadow); + } + } + + .description-field { + flex: 1; + min-width: 0; + } + + & .placeholder-container, + & .image-container, + & .audio-container, + & .video-container, + & .flash-container, + & .oembed-container { + display: flex; + justify-content: center; + width: 100%; + height: 100%; + } + + .image-container { + .image { + width: 100%; + height: 100%; + } + } + + & .flash-container, + & .video-container { + & .flash, + & video { + width: 100%; + height: 100%; + object-fit: contain; + align-self: center; + } + } + + .audio-container { + display: flex; + align-items: flex-end; + + audio { + width: 100%; + height: 100%; + } + } + + .placeholder-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding-top: 0.5em; + } + + + .play-icon { + position: absolute; + font-size: 64px; + top: calc(50% - 32px); + left: calc(50% - 32px); + color: rgba(255, 255, 255, 0.75); + text-shadow: 0 0 2px rgba(0, 0, 0, 0.4); + + &::before { + margin: 0; + } + } + + .attachment-buttons { + display: flex; + position: absolute; + right: 0; + top: 0; + margin-top: 0.5em; + margin-right: 0.5em; + z-index: 1; + + .attachment-button { + padding: 0; + border-radius: $fallback--tooltipRadius; + border-radius: var(--tooltipRadius, $fallback--tooltipRadius); + text-align: center; + width: 2em; + height: 2em; + margin-left: 0.5em; + font-size: 1.25em; + // TODO: theming? hard to theme with unknown background image color + background: rgba(230, 230, 230, 0.7); + + .svg-inline--fa { + color: rgba(0, 0, 0, 0.6); + } + + &:hover .svg-inline--fa { + color: rgba(0, 0, 0, 0.9); + } + } + } + + .oembed-container { + line-height: 1.2em; + flex: 1 0 100%; + width: 100%; + margin-right: 15px; + display: flex; + + img { + width: 100%; + } + + .image { + flex: 1; + img { + border: 0px; + border-radius: 5px; + height: 100%; + object-fit: cover; + } + } + + .text { + flex: 2; + margin: 8px; + word-break: break-all; + h1 { + font-size: 14px; + margin: 0px; + } + } + } + + &.-size-small { + .play-icon { + zoom: 0.5; + opacity: 0.7; + } + + .attachment-buttons { + zoom: 0.7; + opacity: 0.5; + } + } + + &.-editable { + padding: 0.5em; + + & .description-container, + & .attachment-buttons { + margin: 0; + } + } + + &.-placeholder { + display: inline-block; + color: $fallback--link; + color: var(--postLink, $fallback--link); + overflow: hidden; + white-space: nowrap; + height: auto; + line-height: 1.5; + + &:not(.-editable) { + border: none; + } + + &.-editable { + display: flex; + flex-direction: row; + align-items: baseline; + + & .description-container, + & .attachment-buttons { + margin: 0; + padding: 0; + position: relative; + } + + .description-container { + flex: 1; + padding-left: 0.5em; + } + + .attachment-buttons { + order: 99; + align-self: center; + } + } + + a { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } + + svg { + color: inherit; + } + } + + &.-loading { + cursor: progress; + } + + &.-contain-fit { + img, + canvas { + object-fit: contain; + } + } + + &.-cover-fit { + img, + canvas { + object-fit: cover; + } + } +} diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 2c1c168210ba8b3b33b9ef18776b16fe3e1cbe67..5917375984525450dfbee48833a40f52ffd4fbdf 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -1,7 +1,8 @@ <template> - <div + <button v-if="usePlaceholder" - :class="{ 'fullwidth': fullwidth }" + class="Attachment -placeholder button-unstyled" + :class="classNames" @click="openModal" > <a @@ -13,310 +14,251 @@ :title="attachment.description" > <FAIcon :icon="placeholderIconClass" /> - <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }} + <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ edit ? '' : placeholderName }} </a> - </div> - <div - v-else - v-show="!isEmpty" - class="attachment" - :class="{[type]: true, loading, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" - > - <a - v-if="hidden" - class="image-attachment" - :href="attachment.url" - :alt="attachment.description" - :title="attachment.description" - @click.prevent.stop="toggleHidden" + <div + v-if="edit || remove" + class="attachment-buttons" > - <img - :key="nsfwImage" - class="nsfw" - :src="nsfwImage" - :class="{'small': isSmall}" + <button + v-if="remove" + class="button-unstyled attachment-button" + @click.prevent="onRemove" > - <FAIcon - v-if="type === 'video'" - class="play-icon" - icon="play-circle" - /> - </a> - <button - v-if="nsfw && hideNsfwLocal && !hidden" - class="button-unstyled hider" - @click.prevent="toggleHidden" + <FAIcon icon="trash-alt" /> + </button> + </div> + <div + v-if="size !== 'hide' && !hideDescription && (edit || localDescription || showDescription)" + class="description-container" + :class="{ '-static': !edit }" > - <FAIcon icon="times" /> - </button> - - <a - v-if="type === 'image' && (!hidden || preloadImage)" - class="image-attachment" - :class="{'hidden': hidden && preloadImage }" - :href="attachment.url" - target="_blank" - @click="openModal" + <input + v-if="edit" + v-model="localDescription" + type="text" + class="description-field" + :placeholder="$t('post_status.media_description')" + @keydown.enter.prevent="" + > + <p v-else> + {{ localDescription }} + </p> + </div> + </button> + <div + v-else + class="Attachment" + :class="classNames" + > + <div + v-show="!isEmpty" + class="attachment-wrapper" > - <StillImage - class="image" - :referrerpolicy="referrerpolicy" - :mimetype="attachment.mimetype" - :src="attachment.large_thumb_url || attachment.url" - :image-load-handler="onImageLoad" + <a + v-if="hidden" + class="image-container" + :href="attachment.url" :alt="attachment.description" - /> - </a> + :title="attachment.description" + @click.prevent.stop="toggleHidden" + > + <img + :key="nsfwImage" + class="nsfw" + :src="nsfwImage" + > + <FAIcon + v-if="type === 'video'" + class="play-icon" + icon="play-circle" + /> + </a> + <div + v-if="!hidden" + class="attachment-buttons" + > + <button + v-if="type === 'flash' && flashLoaded" + class="button-unstyled attachment-button" + @click.prevent="stopFlash" + :title="$t('status.attachment_stop_flash')" + > + <FAIcon icon="stop" /> + </button> + <button + v-if="attachment.description && size !== 'small' && !edit && type !== 'unknown'" + class="button-unstyled attachment-button" + @click.prevent="toggleDescription" + :title="$t('status.show_attachment_description')" + > + <FAIcon icon="align-right" /> + </button> + <button + v-if="!useModal && type !== 'unknown'" + class="button-unstyled attachment-button" + @click.prevent="openModalForce" + :title="$t('status.show_attachment_in_modal')" + > + <FAIcon icon="search-plus" /> + </button> + <button + v-if="nsfw && hideNsfwLocal" + class="button-unstyled attachment-button" + @click.prevent="toggleHidden" + :title="$t('status.hide_attachment')" + > + <FAIcon icon="times" /> + </button> + <button + v-if="shiftUp" + class="button-unstyled attachment-button" + @click.prevent="onShiftUp" + :title="$t('status.move_up')" + > + <FAIcon icon="chevron-left" /> + </button> + <button + v-if="shiftDn" + class="button-unstyled attachment-button" + @click.prevent="onShiftDn" + :title="$t('status.move_down')" + > + <FAIcon icon="chevron-right" /> + </button> + <button + v-if="remove" + class="button-unstyled attachment-button" + @click.prevent="onRemove" + :title="$t('status.remove_attachment')" + > + <FAIcon icon="trash-alt" /> + </button> + </div> - <a - v-if="type === 'video' && !hidden" - class="video-container" - :class="{'small': isSmall}" - :href="allowPlay ? undefined : attachment.url" - @click="openModal" - > - <VideoAttachment - class="video" - :attachment="attachment" - :controls="allowPlay" - @play="$emit('play')" - @pause="$emit('pause')" - /> - <FAIcon - v-if="!allowPlay" - class="play-icon" - icon="play-circle" - /> - </a> + <a + v-if="type === 'image' && (!hidden || preloadImage)" + class="image-container" + :class="{'-hidden': hidden && preloadImage }" + :href="attachment.url" + target="_blank" + @click.stop.prevent="openModal" + > + <StillImage + class="image" + :referrerpolicy="referrerpolicy" + :mimetype="attachment.mimetype" + :src="attachment.large_thumb_url || attachment.url" + :image-load-handler="onImageLoad" + :alt="attachment.description" + /> + </a> + + <a + v-if="type === 'unknown' && !hidden" + class="placeholder-container" + :href="attachment.url" + target="_blank" + > + <FAIcon size="5x" :icon="placeholderIconClass" /> + <p> + {{ localDescription }} + </p> + </a> + + <component + :is="videoTag" + v-if="type === 'video' && !hidden" + class="video-container" + :class="{ 'button-unstyled': 'isModal' }" + :href="attachment.url" + @click.stop.prevent="openModal" + > + <VideoAttachment + class="video" + :attachment="attachment" + :controls="!useModal" + @play="$emit('play')" + @pause="$emit('pause')" + /> + <FAIcon + v-if="useModal" + class="play-icon" + icon="play-circle" + /> + </component> + + <span + v-if="type === 'audio' && !hidden" + class="audio-container" + :href="attachment.url" + @click.stop.prevent="openModal" + > + <audio + v-if="type === 'audio'" + :src="attachment.url" + :alt="attachment.description" + :title="attachment.description" + controls + @play="$emit('play')" + @pause="$emit('pause')" + /> + </span> - <audio - v-if="type === 'audio'" - :src="attachment.url" - :alt="attachment.description" - :title="attachment.description" - controls - @play="$emit('play')" - @pause="$emit('pause')" - /> + <div + v-if="type === 'html' && attachment.oembed" + class="oembed-container" + @click.prevent="linkClicked" + > + <div + v-if="attachment.thumb_url" + class="image" + > + <img :src="attachment.thumb_url"> + </div> + <div class="text"> + <!-- eslint-disable vue/no-v-html --> + <h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1> + <div v-html="attachment.oembed.oembedHTML" /> + <!-- eslint-enable vue/no-v-html --> + </div> + </div> + <span + v-if="type === 'flash' && !hidden" + class="flash-container" + :href="attachment.url" + @click.stop.prevent="openModal" + > + <Flash + ref="flash" + class="flash" + :src="attachment.large_thumb_url || attachment.url" + @playerOpened="setFlashLoaded(true)" + @playerClosed="setFlashLoaded(false)" + /> + </span> + </div> <div - v-if="type === 'html' && attachment.oembed" - class="oembed" - @click.prevent="linkClicked" + v-if="size !== 'hide' && !hideDescription && (edit || (localDescription && showDescription))" + class="description-container" + :class="{ '-static': !edit }" > - <div - v-if="attachment.thumb_url" - class="image" + <input + v-if="edit" + v-model="localDescription" + type="text" + class="description-field" + :placeholder="$t('post_status.media_description')" + @keydown.enter.prevent="" > - <img :src="attachment.thumb_url"> - </div> - <div class="text"> - <!-- eslint-disable vue/no-v-html --> - <h1><a :href="attachment.url">{{ attachment.oembed.title }}</a></h1> - <div v-html="attachment.oembed.oembedHTML" /> - <!-- eslint-enable vue/no-v-html --> - </div> + <p v-else> + {{ localDescription }} + </p> </div> </div> </template> <script src="./attachment.js"></script> -<style lang="scss"> -@import '../../_variables.scss'; - -.attachments { - display: flex; - flex-wrap: wrap; - - .non-gallery { - max-width: 100%; - } - - .placeholder { - display: inline-block; - padding: 0.3em 1em 0.3em 0; - color: $fallback--link; - color: var(--postLink, $fallback--link); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - max-width: 100%; - - svg { - color: inherit; - } - } - - .nsfw-placeholder { - cursor: pointer; - - &.loading { - cursor: progress; - } - } - - .attachment { - position: relative; - margin-top: 0.5em; - align-self: flex-start; - line-height: 0; - - border-style: solid; - border-width: 1px; - border-radius: $fallback--attachmentRadius; - border-radius: var(--attachmentRadius, $fallback--attachmentRadius); - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - overflow: hidden; - } - - .non-gallery.attachment { - &.video { - flex: 1 0 40%; - } - .nsfw { - height: 260px; - } - .small { - height: 120px; - flex-grow: 0; - } - .video { - height: 260px; - display: flex; - } - video { - max-height: 100%; - object-fit: contain; - } - } - - .fullwidth { - flex-basis: 100%; - } - // fixes small gap below video - &.video { - line-height: 0; - } - - .video-container { - display: flex; - max-height: 100%; - } - - .video { - width: 100%; - height: 100%; - } - - .play-icon { - position: absolute; - font-size: 64px; - top: calc(50% - 32px); - left: calc(50% - 32px); - color: rgba(255, 255, 255, 0.75); - text-shadow: 0 0 2px rgba(0, 0, 0, 0.4); - } - - .play-icon::before { - margin: 0; - } - - &.html { - flex-basis: 90%; - width: 100%; - display: flex; - } - - .hider { - position: absolute; - right: 0; - margin: 10px; - padding: 0; - z-index: 4; - border-radius: $fallback--tooltipRadius; - border-radius: var(--tooltipRadius, $fallback--tooltipRadius); - text-align: center; - width: 2em; - height: 2em; - font-size: 1.25em; - // TODO: theming? hard to theme with unknown background image color - background: rgba(230, 230, 230, 0.7); - .svg-inline--fa { - color: rgba(0, 0, 0, 0.6); - } - &:hover .svg-inline--fa { - color: rgba(0, 0, 0, 0.9); - } - } - - video { - z-index: 0; - } - - audio { - width: 100%; - } - - img.media-upload { - line-height: 0; - max-height: 200px; - max-width: 100%; - } - - .oembed { - line-height: 1.2em; - flex: 1 0 100%; - width: 100%; - margin-right: 15px; - display: flex; - - img { - width: 100%; - } - - .image { - flex: 1; - img { - border: 0px; - border-radius: 5px; - height: 100%; - object-fit: cover; - } - } - - .text { - flex: 2; - margin: 8px; - word-break: break-all; - h1 { - font-size: 14px; - margin: 0px; - } - } - } - - .image-attachment { - &, - & .image { - width: 100%; - height: 100%; - } - - &.hidden { - display: none; - } - - .nsfw { - object-fit: cover; - width: 100%; - height: 100%; - } - - img { - image-orientation: from-image; // NOTE: only FF supports this - } - } -} -</style> +<style src="./attachment.scss" lang="scss"></style> diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js index 87085a2824e8942eb169dcfe29cd635775a40a34..8f41e2fb040a1b66ee25d4763a00c325c176de38 100644 --- a/src/components/basic_user_card/basic_user_card.js +++ b/src/components/basic_user_card/basic_user_card.js @@ -1,5 +1,6 @@ import UserCard from '../user_card/user_card.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' const BasicUserCard = { @@ -13,7 +14,8 @@ const BasicUserCard = { }, components: { UserCard, - UserAvatar + UserAvatar, + RichContent }, methods: { toggleUserExpanded () { diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index 9e4106102daab02580641403d554e712c5d253d7..53deb1df4a3fc9613eda992e68f52c11d0cdfbca 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -25,24 +25,18 @@ :title="user.name" class="basic-user-card-user-name" > - <!-- eslint-disable vue/no-v-html --> - <span - v-if="user.name_html" + <RichContent class="basic-user-card-user-name-value" - v-html="user.name_html" + :html="user.name" + :emoji="user.emoji" /> - <!-- eslint-enable vue/no-v-html --> - <span - v-else - class="basic-user-card-user-name-value" - >{{ user.name }}</span> </div> <div> <router-link 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 index e57fcb91e7a579fa5109ac906b67c9907143028d..b54f5fb2c11cc36174a6e3ded655af465a792ffd 100644 --- 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 index aef58495cd35e83f63d2cafc2c089e395e0339e1..3a26686c4023ba4eb434c7cbe07eb1cd2030bfd6 100644 --- 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_list/chat_list.vue b/src/components/chat_list/chat_list.vue index e23eec135b1a60ccbf2f8d2e1a66e478e294a2cc..f98b7ed294e29bb4b0005552c835ead83f300d60 100644 --- a/src/components/chat_list/chat_list.vue +++ b/src/components/chat_list/chat_list.vue @@ -23,10 +23,7 @@ class="timeline" > <List :items="sortedChatList"> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <ChatListItem :key="item.id" :compact="false" diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js index bee1ad5358b3ee7870cb561645c500f2605978a2..e5032176f3850c62ee1b9a1a161e66fbd1b65117 100644 --- a/src/components/chat_list_item/chat_list_item.js +++ b/src/components/chat_list_item/chat_list_item.js @@ -1,5 +1,5 @@ import { mapState } from 'vuex' -import StatusContent from '../status_content/status_content.vue' +import StatusBody from '../status_content/status_content.vue' import fileType from 'src/services/file_type/file_type.service' import UserAvatar from '../user_avatar/user_avatar.vue' import AvatarList from '../avatar_list/avatar_list.vue' @@ -16,7 +16,7 @@ const ChatListItem = { AvatarList, Timeago, ChatTitle, - StatusContent + StatusBody }, computed: { ...mapState({ @@ -38,12 +38,14 @@ const ChatListItem = { }, messageForStatusContent () { const message = this.chat.lastMessage + const messageEmojis = message ? message.emojis : [] const isYou = message && message.account_id === this.currentUser.id const content = message ? (this.attachmentInfo || message.content) : '' const messagePreview = isYou ? `<i>${this.$t('chats.you')}</i> ${content}` : content return { summary: '', - statusnet_html: messagePreview, + emojis: messageEmojis, + raw_html: messagePreview, text: messagePreview, attachments: [] } diff --git a/src/components/chat_list_item/chat_list_item.scss b/src/components/chat_list_item/chat_list_item.scss index 9e97b28ea9fdfa4cc5872245756151777310121d..57332bed4908bbd245894eedaf51c8c19504cd46 100644 --- a/src/components/chat_list_item/chat_list_item.scss +++ b/src/components/chat_list_item/chat_list_item.scss @@ -77,18 +77,15 @@ border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); } - .StatusContent { - img.emoji { - width: 1.4em; - height: 1.4em; - } + .chat-preview-body { + --emoji-size: 1.4em; } .time-wrapper { line-height: 1.4em; } - .single-line { + .chat-preview-body { padding-right: 1em; } } diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue index cd3f436e28d9472f86bc7a068aba7e4a9b6ed508..c7c0e878fac81f8b8001407acefc14e19288da09 100644 --- a/src/components/chat_list_item/chat_list_item.vue +++ b/src/components/chat_list_item/chat_list_item.vue @@ -29,7 +29,8 @@ </div> </div> <div class="chat-preview"> - <StatusContent + <StatusBody + class="chat-preview-body" :status="messageForStatusContent" :single-line="true" /> diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js index bb380f87fc2d5ad7e6b1c5cb2796e712f5effa7c..eb195bc158066b39903101904ecee8a860d35a16 100644 --- a/src/components/chat_message/chat_message.js +++ b/src/components/chat_message/chat_message.js @@ -57,8 +57,9 @@ const ChatMessage = { messageForStatusContent () { return { summary: '', - statusnet_html: this.message.content, - text: this.message.content, + emojis: this.message.emojis, + raw_html: this.message.content || '', + text: this.message.content || '', attachments: this.message.attachments } }, diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index e4351d3b800814936eab4030c85f39dbef001788..1913479f280d66cdf0f9c5ad591d02ed298a5bb8 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -1,6 +1,7 @@ @import '../../_variables.scss'; .chat-message-wrapper { + &.hovered-message-chain { .animated.Avatar { canvas { @@ -40,6 +41,12 @@ .chat-message { display: flex; padding-bottom: 0.5em; + + .status-body:hover { + --_still-image-img-visibility: visible; + --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; + } } .avatar-wrapper { @@ -62,10 +69,6 @@ &.with-media { width: 100%; - .gallery-row { - overflow: hidden; - } - .status { width: 100%; } @@ -89,8 +92,9 @@ } .without-attachment { - .status-content { - &::after { + .message-content { + // TODO figure out how to do it properly + .RichContent::after { margin-right: 5.4em; content: " "; display: inline-block; @@ -162,6 +166,7 @@ .visible { opacity: 1; } + } .chat-message-date-separator { diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index 0777f8809fd5cb8049e3a81c467c2516d2eab558..d62b831dc50a6e0903d277edf008f78f47158c73 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -50,7 +50,7 @@ @show="menuOpened = true" @close="menuOpened = false" > - <div slot="content"> + <template v-slot:content> <div class="dropdown-menu"> <button class="button-default dropdown-item dropdown-item-icon" @@ -59,26 +59,29 @@ <FAIcon icon="times" /> {{ $t("chats.delete") }} </button> </div> - </div> - <button - slot="trigger" - class="button-default menu-icon" - :title="$t('chats.more')" - > - <FAIcon icon="ellipsis-h" /> - </button> + </template> + <template v-slot:trigger> + <button + class="button-default menu-icon" + :title="$t('chats.more')" + > + <FAIcon icon="ellipsis-h" /> + </button> + </template> </Popover> </div> <StatusContent + class="message-content" :status="messageForStatusContent" :full-content="true" > - <span - slot="footer" - class="created-at" - > - {{ createdAt }} - </span> + <template v-slot:footer> + <span + class="created-at" + > + {{ createdAt }} + </span> + </template> </StatusContent> </div> </div> diff --git a/src/components/chat_message_date/chat_message_date.vue b/src/components/chat_message_date/chat_message_date.vue index 79c346b6172bc0a65b6a1c4baebc005c0225a6d7..98349b753c1470b91a3c31aba40012ce3a58466a 100644 --- 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_title/chat_title.js b/src/components/chat_title/chat_title.js index e424bb1f05b8bbdcc02492f8860a552129db6243..edfbe7a4005ca06378bcb95cebda19444ae6c508 100644 --- 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/chat_title/chat_title.vue b/src/components/chat_title/chat_title.vue index b16ed39d74e4315fe89da8b72ae213e61b6a5f1b..a92028e84c0c9516599f240cd1576756209426b8 100644 --- a/src/components/chat_title/chat_title.vue +++ b/src/components/chat_title/chat_title.vue @@ -1,5 +1,4 @@ <template> - <!-- eslint-disable vue/no-v-html --> <div class="chat-title" :title="title" @@ -14,12 +13,13 @@ height="23px" /> </router-link> - <span + <RichContent class="username" - v-html="htmlTitle" + :title="'@'+user.screen_name_ui" + :html="htmlTitle" + :emoji="user.emoji" /> </div> - <!-- eslint-enable vue/no-v-html --> </template> <script src="./chat_title.js"></script> @@ -34,6 +34,8 @@ white-space: nowrap; align-items: center; + --emoji-size: 14px; + .username { max-width: 100%; text-overflow: ellipsis; @@ -41,14 +43,6 @@ display: inline; word-wrap: break-word; overflow: hidden; - text-overflow: ellipsis; - - .emoji { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain - } } .Avatar { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 353859b8e241b3bd88805b2d107fc66fbd49c21f..3fb26d9239b25f408c71bf4d752c3ff3d33e2327 100644 --- 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/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index 762aa610a9a896f5816067632a2334abaae362bd..304baf9dbfbcdd7a104cf981d8c5744751232a61 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -52,6 +52,7 @@ href="/pleroma/admin/#/login-pleroma" class="nav-icon" target="_blank" + @click.stop > <FAIcon fixed-width diff --git a/src/components/domain_mute_card/domain_mute_card.vue b/src/components/domain_mute_card/domain_mute_card.vue index 3b5aec141b68640be0d575d89a4365cbc4e19634..836688aa0db4c331621ac8319656f350e0a692e5 100644 --- a/src/components/domain_mute_card/domain_mute_card.vue +++ b/src/components/domain_mute_card/domain_mute_card.vue @@ -9,7 +9,7 @@ class="btn button-default" > {{ $t('domain_mute_card.unmute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('domain_mute_card.unmute_progress') }} </template> </ProgressButton> @@ -19,7 +19,7 @@ class="btn button-default" > {{ $t('domain_mute_card.mute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('domain_mute_card.mute_progress') }} </template> </ProgressButton> diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index 2068a598c7bfb250899d876e8f91ff427a74dbe4..902ec384c5f3f43fc0bcecc0b620323ccd8d4586 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -57,6 +57,7 @@ const EmojiInput = { required: true, type: Function }, + // TODO VUE3: change to modelValue, change 'input' event to 'input' value: { /** * Used for v-model @@ -143,32 +144,31 @@ const EmojiInput = { } }, mounted () { - const slots = this.$slots.default - if (!slots || slots.length === 0) return - const input = slots.find(slot => ['input', 'textarea'].includes(slot.tag)) + const { root } = this.$refs + const input = root.querySelector('.emoji-input > input') || root.querySelector('.emoji-input > textarea') if (!input) return this.input = input this.resize() - input.elm.addEventListener('blur', this.onBlur) - input.elm.addEventListener('focus', this.onFocus) - input.elm.addEventListener('paste', this.onPaste) - input.elm.addEventListener('keyup', this.onKeyUp) - input.elm.addEventListener('keydown', this.onKeyDown) - input.elm.addEventListener('click', this.onClickInput) - input.elm.addEventListener('transitionend', this.onTransition) - input.elm.addEventListener('input', this.onInput) + input.addEventListener('blur', this.onBlur) + input.addEventListener('focus', this.onFocus) + input.addEventListener('paste', this.onPaste) + input.addEventListener('keyup', this.onKeyUp) + input.addEventListener('keydown', this.onKeyDown) + input.addEventListener('click', this.onClickInput) + input.addEventListener('transitionend', this.onTransition) + input.addEventListener('input', this.onInput) }, unmounted () { const { input } = this if (input) { - input.elm.removeEventListener('blur', this.onBlur) - input.elm.removeEventListener('focus', this.onFocus) - input.elm.removeEventListener('paste', this.onPaste) - input.elm.removeEventListener('keyup', this.onKeyUp) - input.elm.removeEventListener('keydown', this.onKeyDown) - input.elm.removeEventListener('click', this.onClickInput) - input.elm.removeEventListener('transitionend', this.onTransition) - input.elm.removeEventListener('input', this.onInput) + input.removeEventListener('blur', this.onBlur) + input.removeEventListener('focus', this.onFocus) + input.removeEventListener('paste', this.onPaste) + input.removeEventListener('keyup', this.onKeyUp) + input.removeEventListener('keydown', this.onKeyDown) + input.removeEventListener('click', this.onClickInput) + input.removeEventListener('transitionend', this.onTransition) + input.removeEventListener('input', this.onInput) } }, watch: { @@ -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 @@ -209,11 +216,12 @@ const EmojiInput = { }, 0) }, togglePicker () { - this.input.elm.focus() + this.input.focus() this.showPicker = !this.showPicker if (this.showPicker) { this.scrollIntoView() this.$refs.picker.startEmojiLoad() + this.$nextTick(this.focusPickerInput) } }, replace (replacement) { @@ -254,13 +262,13 @@ const EmojiInput = { this.$emit('input', newValue) const position = this.caret + (insertion + spaceAfter + spaceBefore).length if (!keepOpen) { - this.input.elm.focus() + this.input.focus() } this.$nextTick(function () { // Re-focus inputbox after clicking suggestion // Set selection right after the replacement instead of the very end - this.input.elm.setSelectionRange(position, position) + this.input.setSelectionRange(position, position) this.caret = position }) }, @@ -277,9 +285,9 @@ const EmojiInput = { this.$nextTick(function () { // Re-focus inputbox after clicking suggestion - this.input.elm.focus() + this.input.focus() // Set selection right after the replacement instead of the very end - this.input.elm.setSelectionRange(position, position) + this.input.setSelectionRange(position, position) this.caret = position }) e.preventDefault() @@ -341,7 +349,7 @@ const EmojiInput = { } this.$nextTick(() => { - const { offsetHeight } = this.input.elm + const { offsetHeight } = this.input const { picker } = this.$refs const pickerBottom = picker.$el.getBoundingClientRect().bottom if (pickerBottom > window.innerHeight) { @@ -406,8 +414,8 @@ const EmojiInput = { // Scroll the input element to the position of the cursor this.$nextTick(() => { - this.input.elm.blur() - this.input.elm.focus() + this.input.blur() + this.input.focus() }) } // Disable suggestions hotkeys if suggestions are hidden @@ -436,7 +444,7 @@ const EmojiInput = { // de-focuses the element (i.e. default browser behavior) if (key === 'Escape') { if (!this.temporarilyHideSuggestions) { - this.input.elm.focus() + this.input.focus() } } @@ -472,7 +480,7 @@ const EmojiInput = { if (!panel) return const picker = this.$refs.picker.$el const panelBody = this.$refs['panel-body'] - const { offsetHeight, offsetTop } = this.input.elm + const { offsetHeight, offsetTop } = this.input const offsetBottom = offsetTop + offsetHeight this.setPlacement(panelBody, panel, offsetBottom) @@ -486,7 +494,7 @@ const EmojiInput = { if (this.placement === 'top' || (this.placement === 'auto' && this.overflowsBottom(container))) { target.style.top = 'auto' - target.style.bottom = this.input.elm.offsetHeight + 'px' + target.style.bottom = this.input.offsetHeight + 'px' } }, overflowsBottom (el) { diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index 4becdc4199a3cf0ce846d7df8c66c7d443f7d9c1..aa2950ce8b84beb7e76eaed0d6bb2e0e204c6f88 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -1,5 +1,6 @@ <template> <div + ref="root" v-click-outside="onClickOutside" class="emoji-input" :class="{ 'with-picker': !hideEmojiButton }" @@ -9,6 +10,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 index 14a2b41edfd375b7c553b9a551699d1231c11d67..e8efbd1e0ad3f9f1a674a423c7c82e27fb5d322f 100644 --- 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/export_import/export_import.vue b/src/components/export_import/export_import.vue deleted file mode 100644 index 8ffe34f8fb7c16e9413c569b7a86f979723dfc29..0000000000000000000000000000000000000000 --- a/src/components/export_import/export_import.vue +++ /dev/null @@ -1,102 +0,0 @@ -<template> - <div class="import-export-container"> - <slot name="before" /> - <button - class="btn button-default" - @click="exportData" - > - {{ exportLabel }} - </button> - <button - class="btn button-default" - @click="importData" - > - {{ importLabel }} - </button> - <slot name="afterButtons" /> - <p - v-if="importFailed" - class="alert error" - > - {{ importFailedText }} - </p> - <slot name="afterError" /> - </div> -</template> - -<script> -export default { - props: [ - 'exportObject', - 'importLabel', - 'exportLabel', - 'importFailedText', - 'validator', - 'onImport', - 'onImportFailure' - ], - data () { - return { - importFailed: false - } - }, - methods: { - exportData () { - const stringified = JSON.stringify(this.exportObject, null, 2) // Pretty-print and indent with 2 spaces - - // Create an invisible link with a data url and simulate a click - const e = document.createElement('a') - e.setAttribute('download', 'pleroma_theme.json') - e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified)) - e.style.display = 'none' - - document.body.appendChild(e) - e.click() - document.body.removeChild(e) - }, - importData () { - this.importFailed = false - const filePicker = document.createElement('input') - filePicker.setAttribute('type', 'file') - filePicker.setAttribute('accept', '.json') - - filePicker.addEventListener('change', event => { - if (event.target.files[0]) { - // eslint-disable-next-line no-undef - const reader = new FileReader() - reader.onload = ({ target }) => { - try { - const parsed = JSON.parse(target.result) - const valid = this.validator(parsed) - if (valid) { - this.onImport(parsed) - } else { - this.importFailed = true - // this.onImportFailure(valid) - } - } catch (e) { - // This will happen both if there is a JSON syntax error or the theme is missing components - this.importFailed = true - // this.onImportFailure(e) - } - } - reader.readAsText(event.target.files[0]) - } - }) - - document.body.appendChild(filePicker) - filePicker.click() - document.body.removeChild(filePicker) - } - } -} -</script> - -<style lang="scss"> -.import-export-container { - display: flex; - flex-wrap: wrap; - align-items: baseline; - justify-content: center; -} -</style> diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index e845d8fc5aefa98172b207ff4a2d4e6b40da83d3..a3c3c767b1c9eff66d47bc449ca7682997a528e2 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -7,10 +7,7 @@ :bound-to="{ x: 'container' }" remove-padding > - <div - slot="content" - slot-scope="{close}" - > + <template v-slot:content="{close}"> <div class="dropdown-menu"> <button v-if="canMute && !status.thread_muted" @@ -120,16 +117,15 @@ /><span>{{ $t("user_card.report") }}</span> </button> </div> - </div> - <span - slot="trigger" - class="popover-trigger" - > - <FAIcon - class="fa-scale-110 fa-old-padding" - icon="ellipsis-h" - /> - </span> + </template> + <template v-slot:trigger> + <button class="button-unstyled popover-trigger"> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="ellipsis-h" + /> + </button> + </template> </Popover> </template> @@ -139,6 +135,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/features_panel/features_panel.js b/src/components/features_panel/features_panel.js index 8b142d08308dedb37095c871557706b673d4380e..d177efebe28a301aedd82142843ddba980f611de 100644 --- a/src/components/features_panel/features_panel.js +++ b/src/components/features_panel/features_panel.js @@ -2,7 +2,7 @@ import fileSizeFormatService from '../../services/file_size_format/file_size_for const FeaturesPanel = { computed: { - chat: function () { return this.$store.state.instance.chatAvailable }, + shout: function () { return this.$store.state.instance.shoutAvailable }, pleromaChatMessages: function () { return this.$store.state.instance.pleromaChatMessagesAvailable }, gopher: function () { return this.$store.state.instance.gopherAvailable }, whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled }, diff --git a/src/components/features_panel/features_panel.vue b/src/components/features_panel/features_panel.vue index 9605d09d07185e0a158042855b736ee0f97bdf1a..a58a99afe24f44936f7ada05665a6394f7a90f21 100644 --- a/src/components/features_panel/features_panel.vue +++ b/src/components/features_panel/features_panel.vue @@ -8,8 +8,8 @@ </div> <div class="panel-body features-panel"> <ul> - <li v-if="chat"> - {{ $t('features_panel.chat') }} + <li v-if="shout"> + {{ $t('features_panel.shout') }} </li> <li v-if="pleromaChatMessages"> {{ $t('features_panel.pleroma_chat_messages') }} diff --git a/src/components/flash/flash.js b/src/components/flash/flash.js new file mode 100644 index 0000000000000000000000000000000000000000..87f940a71dfa3d02641efe01e4b391b4f6b99ae6 --- /dev/null +++ b/src/components/flash/flash.js @@ -0,0 +1,53 @@ +import RuffleService from '../../services/ruffle_service/ruffle_service.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faStop, + faExclamationTriangle +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faStop, + faExclamationTriangle +) + +const Flash = { + props: [ 'src' ], + data () { + return { + player: false, // can be true, "hidden", false. hidden = element exists + loaded: false, + ruffleInstance: null + } + }, + methods: { + openPlayer () { + if (this.player) return // prevent double-loading, or re-loading on failure + this.player = 'hidden' + RuffleService.getRuffle().then((ruffle) => { + const player = ruffle.newest().createPlayer() + player.config = { + letterbox: 'on' + } + const container = this.$refs.container + container.appendChild(player) + player.style.width = '100%' + player.style.height = '100%' + player.load(this.src).then(() => { + this.player = true + }).catch((e) => { + console.error('Error loading ruffle', e) + this.player = 'error' + }) + this.ruffleInstance = player + this.$emit('playerOpened') + }) + }, + closePlayer () { + this.ruffleInstance && this.ruffleInstance.remove() + this.player = false + this.$emit('playerClosed') + } + } +} + +export default Flash diff --git a/src/components/flash/flash.vue b/src/components/flash/flash.vue new file mode 100644 index 0000000000000000000000000000000000000000..95f71950936014ac34a014311ecee421141febc0 --- /dev/null +++ b/src/components/flash/flash.vue @@ -0,0 +1,84 @@ +<template> + <div class="Flash"> + <div + v-if="player === true || player === 'hidden'" + ref="container" + class="player" + :class="{ hidden: player === 'hidden' }" + /> + <button + v-if="player !== true" + class="button-unstyled placeholder" + @click="openPlayer" + > + <span + v-if="player === 'hidden'" + class="label" + > + {{ $t('general.loading') }} + </span> + <span + v-if="player === 'error'" + class="label" + > + {{ $t('general.flash_fail') }} + </span> + <span + v-else + class="label" + > + <p> + {{ $t('general.flash_content') }} + </p> + <p> + <FAIcon icon="exclamation-triangle" /> + {{ $t('general.flash_security') }} + </p> + </span> + </button> + </div> +</template> + +<script src="./flash.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; +.Flash { + display: inline-block; + width: 100%; + height: 100%; + position: relative; + + .player { + height: 100%; + width: 100%; + } + + .placeholder { + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg); + color: var(--link); + } + + .hider { + top: 0; + } + + .label { + text-align: center; + flex: 1 1 0; + line-height: 1.2; + white-space: normal; + word-wrap: normal; + } + + .hidden { + display: none; + visibility: 'hidden'; + } +} +</style> diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js index 95e7cb6ba7fdb1c95c4f461d684ff80162da1c12..3edbcb86fb079637126c47cce1789cd42faa8594 100644 --- a/src/components/follow_button/follow_button.js +++ b/src/components/follow_button/follow_button.js @@ -1,6 +1,6 @@ import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' export default { - props: ['relationship', 'labelFollowing', 'buttonClass'], + props: ['relationship', 'user', 'labelFollowing', 'buttonClass'], data () { return { inProgress: false @@ -14,7 +14,7 @@ export default { if (this.inProgress || this.relationship.following) { return this.$t('user_card.follow_unfollow') } else if (this.relationship.requested) { - return this.$t('user_card.follow_again') + return this.$t('user_card.follow_cancel') } else { return this.$t('user_card.follow') } @@ -29,11 +29,14 @@ export default { } else { return this.$t('user_card.follow') } + }, + disabled () { + return this.inProgress || this.user.deactivated } }, methods: { onClick () { - this.relationship.following ? this.unfollow() : this.follow() + this.relationship.following || this.relationship.requested ? this.unfollow() : this.follow() }, follow () { this.inProgress = true diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue index 7f85f1d76f0c74d07b8f13ed4260bb9c1b487dd4..965d5256a622813fcfa01afe36fae984da72a8e0 100644 --- a/src/components/follow_button/follow_button.vue +++ b/src/components/follow_button/follow_button.vue @@ -2,7 +2,7 @@ <button class="btn button-default follow-button" :class="{ toggled: isPressed }" - :disabled="inProgress" + :disabled="disabled" :title="title" @click="onClick" > diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue index b503783fd68e0390bf79636e2f3771840d08fdd1..895a8fa3685f2382ab5129f9f5045b296f48202c 100644 --- a/src/components/follow_card/follow_card.vue +++ b/src/components/follow_card/follow_card.vue @@ -20,6 +20,7 @@ :relationship="relationship" :label-following="$t('user_card.follow_unfollow')" class="follow-card-follow-button" + :user="user" /> </template> </div> diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js index 6274780b866cb808afc76d86400627b7c35189e8..137ef9c0babd7cc54d121c3eeae5b3dcf5f63c7c 100644 --- a/src/components/font_control/font_control.js +++ b/src/components/font_control/font_control.js @@ -1,14 +1,10 @@ import { set } from 'vue' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) +import Select from '../select/select.vue' export default { + components: { + Select + }, props: [ 'name', 'label', 'value', 'fallback', 'options', 'no-inherit' ], diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue index dd117ec03cbbb84d68173e2405e4ce66b75f6c70..296050841b008396f62e89597e99ff437f54343d 100644 --- a/src/components/font_control/font_control.vue +++ b/src/components/font_control/font_control.vue @@ -22,30 +22,20 @@ class="opt-l" :for="name + '-o'" /> - <label - :for="name + '-font-switcher'" - class="select" + <Select + :id="name + '-font-switcher'" + v-model="preset" :disabled="!present" + class="font-switcher" > - <select - :id="name + '-font-switcher'" - v-model="preset" - :disabled="!present" - class="font-switcher" + <option + v-for="option in availableOptions" + :key="option" + :value="option" > - <option - v-for="option in availableOptions" - :key="option" - :value="option" - > - {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} + </option> + </Select> <input v-if="isCustom" :id="name" @@ -65,7 +55,8 @@ min-width: 10em; } &.custom { - .select { + /* TODO Should make proper joiners... */ + .font-switcher { border-top-right-radius: 0; border-bottom-right-radius: 0; } diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js index f856fd0a549cd8a86337f6e14d524496f3f61f9a..094b3e5703f0e168556ada486fb44c6331b8e5d6 100644 --- a/src/components/gallery/gallery.js +++ b/src/components/gallery/gallery.js @@ -1,15 +1,26 @@ import Attachment from '../attachment/attachment.vue' -import { chunk, last, dropRight, sumBy } from 'lodash' +import { sumBy } from 'lodash' const Gallery = { props: [ 'attachments', + 'limitRows', + 'descriptions', + 'limit', 'nsfw', - 'setMedia' + 'setMedia', + 'size', + 'editable', + 'removeAttachment', + 'shiftUpAttachment', + 'shiftDnAttachment', + 'editAttachment', + 'grid' ], data () { return { - sizes: {} + sizes: {}, + hidingLong: true } }, components: { Attachment }, @@ -18,26 +29,70 @@ const Gallery = { if (!this.attachments) { return [] } - const rows = chunk(this.attachments, 3) - if (last(rows).length === 1 && rows.length > 1) { - // if 1 attachment on last row -> add it to the previous row instead - const lastAttachment = last(rows)[0] - const allButLastRow = dropRight(rows) - last(allButLastRow).push(lastAttachment) - return allButLastRow + const attachments = this.limit > 0 + ? this.attachments.slice(0, this.limit) + : this.attachments + if (this.size === 'hide') { + return attachments.map(item => ({ minimal: true, items: [item] })) } + const rows = this.grid + ? [{ grid: true, items: attachments }] + : attachments.reduce((acc, attachment, i) => { + if (attachment.mimetype.includes('audio')) { + return [...acc, { audio: true, items: [attachment] }, { items: [] }] + } + if (!( + attachment.mimetype.includes('image') || + attachment.mimetype.includes('video') || + attachment.mimetype.includes('flash') + )) { + return [...acc, { minimal: true, items: [attachment] }, { items: [] }] + } + const maxPerRow = 3 + const attachmentsRemaining = this.attachments.length - i + 1 + const currentRow = acc[acc.length - 1].items + currentRow.push(attachment) + if (currentRow.length >= maxPerRow && attachmentsRemaining > maxPerRow) { + return [...acc, { items: [] }] + } else { + return acc + } + }, [{ items: [] }]).filter(_ => _.items.length > 0) return rows }, - useContainFit () { - return this.$store.getters.mergedConfig.useContainFit + attachmentsDimensionalScore () { + return this.rows.reduce((acc, row) => { + let size = 0 + if (row.minimal) { + size += 1 / 8 + } else if (row.audio) { + size += 1 / 4 + } else { + size += 1 / (row.items.length + 0.6) + } + return acc + size + }, 0) + }, + tooManyAttachments () { + if (this.editable || this.size === 'small') { + return false + } else if (this.size === 'hide') { + return this.attachments.length > 8 + } else { + return this.attachmentsDimensionalScore > 1 + } } }, methods: { - onNaturalSizeLoad (id, size) { - this.$set(this.sizes, id, size) + onNaturalSizeLoad ({ id, width, height }) { + this.$set(this.sizes, id, { width, height }) }, - rowStyle (itemsPerRow) { - return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` } + rowStyle (row) { + if (row.audio) { + return { 'padding-bottom': '25%' } // fixed reduced height for audio + } else if (!row.minimal && !row.grid) { + return { 'padding-bottom': `${(100 / (row.items.length + 0.6))}%` } + } }, itemStyle (id, row) { const total = sumBy(row, item => this.getAspectRatio(item.id)) @@ -46,6 +101,16 @@ const Gallery = { getAspectRatio (id) { const size = this.sizes[id] return size ? size.width / size.height : 1 + }, + toggleHidingLong (event) { + this.hidingLong = event + }, + openGallery () { + this.$store.dispatch('setMedia', this.attachments) + this.$store.dispatch('setCurrentMedia', this.attachments[0]) + }, + onMedia () { + this.$store.dispatch('setMedia', this.attachments) } } } diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index ca91c9c11c1f59a6895f5f741e2b21221e304cf1..f9cad8a9c15ea3d180008fa9ce89bf6104cf34e7 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -1,26 +1,84 @@ <template> <div ref="galleryContainer" - style="width: 100%;" + class="Gallery" + :class="{ '-long': tooManyAttachments && hidingLong }" > + <div class="gallery-rows"> + <div + v-for="(row, rowIndex) in rows" + :key="rowIndex" + class="gallery-row" + :style="rowStyle(row)" + :class="{ '-audio': row.audio, '-minimal': row.minimal, '-grid': grid }" + > + <div + class="gallery-row-inner" + :class="{ '-grid': grid }" + > + <Attachment + v-for="(attachment, attachmentIndex) in row.items" + :key="attachment.id" + class="gallery-item" + :nsfw="nsfw" + :attachment="attachment" + :allow-play="false" + :size="size" + :editable="editable" + :remove="removeAttachment" + :shiftUp="!(attachmentIndex === 0 && rowIndex === 0) && shiftUpAttachment" + :shiftDn="!(attachmentIndex === row.items.length - 1 && rowIndex === rows.length - 1) && shiftDnAttachment" + :edit="editAttachment" + :description="descriptions && descriptions[attachment.id]" + :hide-description="size === 'small' || tooManyAttachments && hidingLong" + :style="itemStyle(attachment.id, row.items)" + @setMedia="onMedia" + @naturalSizeLoad="onNaturalSizeLoad" + /> + </div> + </div> + </div> <div - v-for="(row, index) in rows" - :key="index" - class="gallery-row" - :style="rowStyle(row.length)" - :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }" + v-if="tooManyAttachments" + class="many-attachments" > - <div class="gallery-row-inner"> - <attachment - v-for="attachment in row" - :key="attachment.id" - :set-media="setMedia" - :nsfw="nsfw" - :attachment="attachment" - :allow-play="false" - :natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)" - :style="itemStyle(attachment.id, row)" - /> + <div class="many-attachments-text"> + {{ $t("status.many_attachments", { number: attachments.length }) }} + </div> + <div class="many-attachments-buttons"> + <span + v-if="!hidingLong" + class="many-attachments-button" + > + <button + class="button-unstyled -link" + @click="toggleHidingLong(true)" + > + {{ $t("status.collapse_attachments") }} + </button> + </span> + <span + v-if="hidingLong" + class="many-attachments-button" + > + <button + class="button-unstyled -link" + @click="toggleHidingLong(false)" + > + {{ $t("status.show_all_attachments") }} + </button> + </span> + <span + v-if="hidingLong" + class="many-attachments-button" + > + <button + class="button-unstyled -link" + @click="openGallery" + > + {{ $t("status.open_gallery") }} + </button> + </span> </div> </div> </div> @@ -31,12 +89,66 @@ <style lang="scss"> @import '../../_variables.scss'; -.gallery-row { - position: relative; - height: 0; - width: 100%; - flex-grow: 1; - margin-top: 0.5em; +.Gallery { + .gallery-rows { + display: flex; + flex-direction: column; + } + + .gallery-row { + position: relative; + height: 0; + width: 100%; + flex-grow: 1; + + &:not(:first-child) { + margin-top: 0.5em; + } + } + + &.-long { + .gallery-rows { + max-height: 25em; + overflow: hidden; + mask: + linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, + linear-gradient(to top, white, white); + + /* Autoprefixed seem to ignore this one, and also syntax is different */ + -webkit-mask-composite: xor; + mask-composite: exclude; + } + } + + .many-attachments-text { + text-align: center; + line-height: 2; + } + + .many-attachments-buttons { + display: flex; + } + + .many-attachments-button { + display: flex; + flex: 1; + justify-content: center; + line-height: 2; + + button { + padding: 0 2em; + } + } + + .gallery-row { + &.-grid, + &.-minimal { + height: auto; + .gallery-row-inner { + position: relative; + } + } + } .gallery-row-inner { position: absolute; @@ -48,9 +160,24 @@ flex-direction: row; flex-wrap: nowrap; align-content: stretch; + + &.-grid { + width: 100%; + height: auto; + position: relative; + display: grid; + grid-column-gap: 0.5em; + grid-row-gap: 0.5em; + grid-template-columns: repeat(auto-fill, minmax(15em, 1fr)); + + .gallery-item { + margin: 0; + height: 200px; + } + } } - .gallery-row-inner .attachment { + .gallery-item { margin: 0 0.5em 0 0; flex-grow: 1; height: 100%; @@ -61,32 +188,5 @@ margin: 0; } } - - .image-attachment { - width: 100%; - height: 100%; - } - - .video-container { - height: 100%; - } - - &.contain-fit { - img, - video, - canvas { - object-fit: contain; - height: 100%; - } - } - - &.cover-fit { - img, - video, - canvas { - object-fit: cover; - } - } } - </style> diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue index 049e23dbb01033225c2ba9e0627253d8bf14a70a..a45f4586518c71ca584ca7e6013544a5b382205f 100644 --- a/src/components/global_notice_list/global_notice_list.vue +++ b/src/components/global_notice_list/global_notice_list.vue @@ -71,6 +71,14 @@ } } + .global-success { + background-color: var(--alertPopupSuccess, $fallback--cGreen); + color: var(--alertPopupSuccessText, $fallback--text); + .svg-inline--fa { + color: var(--alertPopupSuccessText, $fallback--text); + } + } + .global-info { background-color: var(--alertPopupNeutral, $fallback--fg); color: var(--alertPopupNeutralText, $fallback--text); diff --git a/src/components/hashtag_link/hashtag_link.js b/src/components/hashtag_link/hashtag_link.js new file mode 100644 index 0000000000000000000000000000000000000000..a2433c2abdbe7a7b0141d049843853fcf0241313 --- /dev/null +++ b/src/components/hashtag_link/hashtag_link.js @@ -0,0 +1,36 @@ +import { extractTagFromUrl } from 'src/services/matcher/matcher.service.js' + +const HashtagLink = { + name: 'HashtagLink', + props: { + url: { + required: true, + type: String + }, + content: { + required: true, + type: String + }, + tag: { + required: false, + type: String, + default: '' + } + }, + methods: { + onClick () { + const tag = this.tag || extractTagFromUrl(this.url) + if (tag) { + const link = this.generateTagLink(tag) + this.$router.push(link) + } else { + window.open(this.url, '_blank') + } + }, + generateTagLink (tag) { + return `/tag/${tag}` + } + } +} + +export default HashtagLink diff --git a/src/components/hashtag_link/hashtag_link.scss b/src/components/hashtag_link/hashtag_link.scss new file mode 100644 index 0000000000000000000000000000000000000000..78e8fb99448c2c50c2bc9c8c2d7223d9e744f0d6 --- /dev/null +++ b/src/components/hashtag_link/hashtag_link.scss @@ -0,0 +1,6 @@ +.HashtagLink { + position: relative; + white-space: normal; + display: inline-block; + color: var(--link); +} diff --git a/src/components/hashtag_link/hashtag_link.vue b/src/components/hashtag_link/hashtag_link.vue new file mode 100644 index 0000000000000000000000000000000000000000..918ed26be5fb15a2f9535e05f8b1aeac45623388 --- /dev/null +++ b/src/components/hashtag_link/hashtag_link.vue @@ -0,0 +1,19 @@ +<template> + <span + class="HashtagLink" + > + <!-- eslint-disable vue/no-v-html --> + <a + :href="url" + class="original" + target="_blank" + @click.prevent="onClick" + v-html="content" + /> + <!-- eslint-enable vue/no-v-html --> + </span> +</template> + +<script src="./hashtag_link.js"/> + +<style lang="scss" src="./hashtag_link.scss"/> diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index dc3bd4088c40bae89bbf16ed770aef8b9950a10e..cf307a24e225c85aa416dd6c57fa743dd6140ef3 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -3,27 +3,18 @@ <label for="interface-language-switcher"> {{ $t('settings.interfaceLanguage') }} </label> - <label - for="interface-language-switcher" - class="select" + <Select + id="interface-language-switcher" + v-model="language" > - <select - id="interface-language-switcher" - v-model="language" + <option + v-for="lang in languages" + :key="lang.code" + :value="lang.code" > - <option - v-for="lang in languages" - :key="lang.code" - :value="lang.code" - > - {{ lang.name }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ lang.name }} + </option> + </Select> </div> </template> @@ -32,16 +23,12 @@ 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' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) +import Select from '../select/select.vue' export default { + components: { + Select + }, computed: { languages () { return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name)) diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index e7384c93b3f683c89b10a6b550ed70d3e0b99b97..b8bce73063b6604c796e69c9430cc7724d25b3ef 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -3,22 +3,31 @@ import VideoAttachment from '../video_attachment/video_attachment.vue' import Modal from '../modal/modal.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import GestureService from '../../services/gesture_service/gesture_service' +import Flash from 'src/components/flash/flash.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faChevronLeft, - faChevronRight + faChevronRight, + faCircleNotch } from '@fortawesome/free-solid-svg-icons' library.add( faChevronLeft, - faChevronRight + faChevronRight, + faCircleNotch ) const MediaModal = { components: { StillImage, VideoAttachment, - Modal + Modal, + Flash + }, + data () { + return { + loading: false + } }, computed: { showing () { @@ -27,6 +36,9 @@ const MediaModal = { media () { return this.$store.state.mediaViewer.media }, + description () { + return this.currentMedia.description + }, currentIndex () { return this.$store.state.mediaViewer.currentIndex }, @@ -37,7 +49,7 @@ const MediaModal = { return this.media.length > 1 }, type () { - return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null + return this.currentMedia ? this.getType(this.currentMedia) : null } }, created () { @@ -53,6 +65,9 @@ const MediaModal = { ) }, methods: { + getType (media) { + return fileTypeService.fileType(media.mimetype) + }, mediaTouchStart (e) { GestureService.beginSwipe(e, this.mediaSwipeGestureRight) GestureService.beginSwipe(e, this.mediaSwipeGestureLeft) @@ -67,15 +82,26 @@ const MediaModal = { goPrev () { if (this.canNavigate) { const prevIndex = this.currentIndex === 0 ? this.media.length - 1 : (this.currentIndex - 1) - this.$store.dispatch('setCurrent', this.media[prevIndex]) + const newMedia = this.media[prevIndex] + if (this.getType(newMedia) === 'image') { + this.loading = true + } + this.$store.dispatch('setCurrentMedia', newMedia) } }, goNext () { if (this.canNavigate) { const nextIndex = this.currentIndex === this.media.length - 1 ? 0 : (this.currentIndex + 1) - this.$store.dispatch('setCurrent', this.media[nextIndex]) + const newMedia = this.media[nextIndex] + if (this.getType(newMedia) === 'image') { + this.loading = true + } + this.$store.dispatch('setCurrentMedia', newMedia) } }, + onImageLoaded () { + this.loading = false + }, handleKeyupEvent (e) { if (this.showing && e.keyCode === 27) { // escape this.hide() diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index ea7f7a7f56d498dfa46b24a1ec9d41aae00dee6d..8680267bf95aeb88e3d51f471901c12c54c5faf8 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -6,6 +6,7 @@ > <img v-if="type === 'image'" + :class="{ loading }" class="modal-image" :src="currentMedia.url" :alt="currentMedia.description" @@ -13,6 +14,7 @@ @touchstart.stop="mediaTouchStart" @touchmove.stop="mediaTouchMove" @click="hide" + @load="onImageLoaded" > <VideoAttachment v-if="type === 'video'" @@ -28,6 +30,13 @@ :title="currentMedia.description" controls /> + <Flash + v-if="type === 'flash'" + class="modal-image" + :src="currentMedia.url" + :alt="currentMedia.description" + :title="currentMedia.description" + /> <button v-if="canNavigate" :title="$t('media_modal.previous')" @@ -50,6 +59,27 @@ icon="chevron-right" /> </button> + <span + v-if="description" + class="description" + > + {{ description }} + </span> + <span + class="counter" + > + {{ $tc('media_modal.counter', currentIndex + 1, { current: currentIndex + 1, total: media.length }) }} + </span> + <span + v-if="loading" + class="loading-spinner" + > + <FAIcon + spin + icon="circle-notch" + size="5x" + /> + </span> </Modal> </template> @@ -58,6 +88,7 @@ <style lang="scss"> .modal-view.media-modal-view { z-index: 1001; + flex-direction: column; .modal-view-button-arrow { opacity: 0.75; @@ -67,59 +98,108 @@ outline: none; box-shadow: none; } + &:hover { 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 -} +.media-modal-view { + @keyframes media-fadein { + from { + opacity: 0; + } + to { + opacity: 1; + } + } -.modal-view-button-arrow { - position: absolute; - display: block; - top: 50%; - margin-top: -50px; - width: 70px; - height: 100px; - border: 0; - padding: 0; - opacity: 0; - box-shadow: none; - background: none; - appearance: none; - overflow: visible; - cursor: pointer; - transition: opacity 333ms cubic-bezier(.4,0,.22,1); - - .arrow-icon { - position: absolute; - top: 35px; - height: 30px; - width: 32px; - font-size: 14px; - line-height: 30px; - color: #FFF; - text-align: center; - background-color: rgba(0,0,0,.3); + .description, + .counter { + /* Hardcoded since background is also hardcoded */ + color: white; + margin-top: 1em; + text-shadow: 0 0 10px black, 0 0 10px black; + padding: 0.2em 2em; } - &--prev { - left: 0; - .arrow-icon { - left: 6px; + .description { + flex: 0 0 auto; + overflow-y: auto; + min-height: 1em; + max-width: 500px; + max-height: 9.5em; + word-break: break-all; + } + + .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; + + &.loading { + opacity: 0.5; + } + } + + .loading-spinner { + width: 100%; + height: 100%; + position: absolute; + pointer-events: none; + display: flex; + justify-content: center; + align-items: center; + + svg { + color: white; } } - &--next { - right: 0; + .modal-view-button-arrow { + position: absolute; + display: block; + top: 50%; + margin-top: -50px; + width: 70px; + height: 100px; + border: 0; + padding: 0; + opacity: 0; + box-shadow: none; + background: none; + appearance: none; + overflow: visible; + cursor: pointer; + transition: opacity 333ms cubic-bezier(.4,0,.22,1); + .arrow-icon { - right: 6px; + position: absolute; + top: 35px; + height: 30px; + width: 32px; + font-size: 14px; + line-height: 30px; + color: #FFF; + text-align: center; + background-color: rgba(0,0,0,.3); + } + + &--prev { + left: 0; + .arrow-icon { + left: 6px; + } + } + + &--next { + right: 0; + .arrow-icon { + right: 6px; + } } } } diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js new file mode 100644 index 0000000000000000000000000000000000000000..55eea19520a4a664a5dd82f27f3f71b1811203df --- /dev/null +++ b/src/components/mention_link/mention_link.js @@ -0,0 +1,134 @@ +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { mapGetters, mapState } from 'vuex' +import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' +import UserAvatar from '../user_avatar/user_avatar.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faAt +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faAt +) + +const MentionLink = { + name: 'MentionLink', + components: { + UserAvatar + }, + props: { + url: { + required: true, + type: String + }, + content: { + required: true, + type: String + }, + userId: { + required: false, + type: String + }, + userScreenName: { + required: false, + type: String + } + }, + methods: { + onClick () { + const link = generateProfileLink( + this.userId || this.user.id, + this.userScreenName || this.user.screen_name + ) + this.$router.push(link) + } + }, + computed: { + user () { + return this.url && this.$store && this.$store.getters.findUserByUrl(this.url) + }, + isYou () { + // FIXME why user !== currentUser??? + return this.user && this.user.id === this.currentUser.id + }, + userName () { + return this.user && this.userNameFullUi.split('@')[0] + }, + serverName () { + // XXX assumed that domain does not contain @ + return this.user && (this.userNameFullUi.split('@')[1] || this.$store.getters.instanceDomain) + }, + userNameFull () { + return this.user && this.user.screen_name + }, + userNameFullUi () { + return this.user && this.user.screen_name_ui + }, + highlight () { + return this.user && this.mergedConfig.highlight[this.user.screen_name] + }, + highlightType () { + return this.highlight && ('-' + this.highlight.type) + }, + highlightClass () { + if (this.highlight) return highlightClass(this.user) + }, + style () { + if (this.highlight) { + const { + backgroundColor, + backgroundPosition, + backgroundImage, + ...rest + } = highlightStyle(this.highlight) + return rest + } + }, + classnames () { + return [ + { + '-you': this.isYou && this.shouldBoldenYou, + '-highlighted': this.highlight + }, + this.highlightType + ] + }, + useAtIcon () { + return this.mergedConfig.useAtIcon + }, + isRemote () { + return this.userName !== this.userNameFull + }, + shouldShowFullUserName () { + const conf = this.mergedConfig.mentionLinkDisplay + if (conf === 'short') { + return false + } else if (conf === 'full') { + return true + } else { // full_for_remote + return this.isRemote + } + }, + shouldShowTooltip () { + return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote + }, + shouldShowAvatar () { + return this.mergedConfig.mentionLinkShowAvatar + }, + shouldShowYous () { + return this.mergedConfig.mentionLinkShowYous + }, + shouldBoldenYou () { + return this.mergedConfig.mentionLinkBoldenYou + }, + shouldFadeDomain () { + return this.mergedConfig.mentionLinkFadeDomain + }, + ...mapGetters(['mergedConfig']), + ...mapState({ + currentUser: state => state.users.currentUser + }) + } +} + +export default MentionLink diff --git a/src/components/mention_link/mention_link.scss b/src/components/mention_link/mention_link.scss new file mode 100644 index 0000000000000000000000000000000000000000..a43262962cc0afa271b32a86841af9869edb9108 --- /dev/null +++ b/src/components/mention_link/mention_link.scss @@ -0,0 +1,116 @@ +@import '../../_variables.scss'; + +.MentionLink { + position: relative; + white-space: normal; + display: inline; + color: var(--link); + word-break: normal; + + & .new, + & .original { + display: inline; + border-radius: 2px; + } + + .mention-avatar { + border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); + width: 1.5em; + height: 1.5em; + vertical-align: middle; + user-select: none; + margin-right: 0.2em; + } + + .full { + position: absolute; + display: inline-block; + pointer-events: none; + opacity: 0; + top: 100%; + left: 0; + height: 100%; + word-wrap: normal; + white-space: nowrap; + transition: opacity 0.2s ease; + z-index: 1; + margin-top: 0.25em; + padding: 0.5em; + user-select: all; + } + + & .short.-with-tooltip, + & .you { + user-select: none; + } + + & .short, + & .full { + white-space: nowrap; + } + + .shortName { + white-space: normal; + } + + .new { + &.-you { + & .shortName, + & .full { + font-weight: 600; + } + } + + .at { + color: var(--link); + opacity: 0.8; + display: inline-block; + height: 50%; + line-height: 1; + padding: 0 0.1em; + vertical-align: -25%; + margin: 0; + } + + &.-striped { + & .shortName, + & .full { + background-image: + repeating-linear-gradient( + 135deg, + var(--____highlight-tintColor), + var(--____highlight-tintColor) 5px, + var(--____highlight-tintColor2) 5px, + var(--____highlight-tintColor2) 10px + ); + } + } + + &.-solid { + & .shortName, + & .full { + background-image: linear-gradient(var(--____highlight-tintColor2), var(--____highlight-tintColor2)); + } + } + + &.-side { + & .shortName, + & .userNameFull { + box-shadow: 0 -5px 3px -4px inset var(--____highlight-solidColor); + } + } + } + + &:hover .new .full { + opacity: 1; + pointer-events: initial; + } + + .serverName.-faded { + color: var(--faintLink, $fallback--link); + } + + .full .-faded { + color: var(--faint, $fallback--faint); + } +} diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue new file mode 100644 index 0000000000000000000000000000000000000000..3562f51107ad3ab874ff840468ff780244e4d40c --- /dev/null +++ b/src/components/mention_link/mention_link.vue @@ -0,0 +1,75 @@ +<template> + <span + class="MentionLink" + > + <!-- eslint-disable vue/no-v-html --> + <a + v-if="!user" + :href="url" + class="original" + target="_blank" + v-html="content" + /><!-- eslint-enable vue/no-v-html --><span + v-if="user" + class="new" + :style="style" + :class="classnames" + > + <a + class="short button-unstyled" + :class="{ '-with-tooltip': shouldShowTooltip }" + :href="url" + @click.prevent="onClick" + > + <!-- eslint-disable vue/no-v-html --> + <UserAvatar + v-if="shouldShowAvatar" + class="mention-avatar" + :user="user" + /><span + class="shortName" + ><FAIcon + v-if="useAtIcon" + size="sm" + icon="at" + class="at" + />{{ !useAtIcon ? '@' : '' }}<span + class="userName" + v-html="userName" + /><span + v-if="shouldShowFullUserName" + class="serverName" + :class="{ '-faded': shouldFadeDomain }" + v-html="'@' + serverName" + /></span><span + v-if="isYou && shouldShowYous" + :class="{ '-you': shouldBoldenYou }" + > {{ $t('status.you') }}</span> + <!-- eslint-enable vue/no-v-html --> + </a><span + v-if="shouldShowTooltip" + class="full popover-default" + :class="[highlightType]" + > + <span + class="userNameFull" + > + <!-- eslint-disable vue/no-v-html --> + @<span + class="userName" + v-html="userName" + /><span + class="serverName" + :class="{ '-faded': shouldFadeDomain }" + v-html="'@' + serverName" + /> + <!-- eslint-enable vue/no-v-html --> + </span> + </span> + </span> + </span> +</template> + +<script src="./mention_link.js"/> + +<style lang="scss" src="./mention_link.scss"/> diff --git a/src/components/mentions_line/mentions_line.js b/src/components/mentions_line/mentions_line.js new file mode 100644 index 0000000000000000000000000000000000000000..a4a0c7246fd7c4134594384cdb0905309cce5e57 --- /dev/null +++ b/src/components/mentions_line/mentions_line.js @@ -0,0 +1,37 @@ +import MentionLink from 'src/components/mention_link/mention_link.vue' +import { mapGetters } from 'vuex' + +export const MENTIONS_LIMIT = 5 + +const MentionsLine = { + name: 'MentionsLine', + props: { + mentions: { + required: true, + type: Array + } + }, + data: () => ({ expanded: false }), + components: { + MentionLink + }, + computed: { + mentionsComputed () { + return this.mentions.slice(0, MENTIONS_LIMIT) + }, + extraMentions () { + return this.mentions.slice(MENTIONS_LIMIT) + }, + manyMentions () { + return this.extraMentions.length > 0 + }, + ...mapGetters(['mergedConfig']) + }, + methods: { + toggleShowMore () { + this.expanded = !this.expanded + } + } +} + +export default MentionsLine diff --git a/src/components/mentions_line/mentions_line.scss b/src/components/mentions_line/mentions_line.scss new file mode 100644 index 0000000000000000000000000000000000000000..9a622e75f090778bcd4a882494697a4305467b98 --- /dev/null +++ b/src/components/mentions_line/mentions_line.scss @@ -0,0 +1,13 @@ +.MentionsLine { + word-break: break-all; + + .mention-link:not(:first-child)::before { + content: ' '; + } + + .showMoreLess { + margin-left: 0.5em; + white-space: normal; + color: var(--link); + } +} diff --git a/src/components/mentions_line/mentions_line.vue b/src/components/mentions_line/mentions_line.vue new file mode 100644 index 0000000000000000000000000000000000000000..f375e3b0921f1ca4d872618b83e5396773c0a1d6 --- /dev/null +++ b/src/components/mentions_line/mentions_line.vue @@ -0,0 +1,43 @@ +<template> + <span class="MentionsLine"> + <MentionLink + v-for="mention in mentionsComputed" + :key="mention.index" + class="mention-link" + :content="mention.content" + :url="mention.url" + :first-mention="false" + /><span + v-if="manyMentions" + class="extraMentions" + > + <span + v-if="expanded" + class="fullExtraMentions" + > + <MentionLink + v-for="mention in extraMentions" + :key="mention.index" + class="mention-link" + :content="mention.content" + :url="mention.url" + :first-mention="false" + /> + </span><button + v-if="!expanded" + class="button-unstyled showMoreLess" + @click="toggleShowMore" + > + {{ $t('status.plus_more', { number: extraMentions.length }) }} + </button><button + v-if="expanded" + class="button-unstyled showMoreLess" + @click="toggleShowMore" + > + {{ $t('general.show_less') }} + </button> + </span> + </span> +</template> +<script src="./mentions_line.js" ></script> +<style lang="scss" src="./mentions_line.scss" /> diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue index 0bf68e27cbfee4399a92a4ea94435658375fc5e2..7c594228374a63044d513ae948741fb09a165976 100644 --- 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 index 79230148ee34b9d3cd20f318b75a450e38cc518f..4ee139924509d259ab997b35561f9fb7fbed009e 100644 --- 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/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js index 366ea89c23ca6edd3ac099f05182e3e0f7d97fef..d27fb3b801ba4dd8762e1133a795ee53ec379ae3 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.js +++ b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -44,6 +44,9 @@ const MobilePostStatusButton = { return this.autohideFloatingPostButton && (this.hidden || this.inputActive) }, + isPersistent () { + return !!this.$store.getters.mergedConfig.showNewPostButton + }, autohideFloatingPostButton () { return !!this.$store.getters.mergedConfig.autohideFloatingPostButton } diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue index 767f82446b462063112f904da32a10b05dad0aae..37becf4c4a705c1b7042188088ee826277b9e974 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.vue +++ b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -2,7 +2,7 @@ <div v-if="isLoggedIn"> <button class="button-default new-status-button" - :class="{ 'hidden': isHidden }" + :class="{ 'hidden': isHidden, 'always-show': isPersistent }" @click="openPostForm" > <FAIcon icon="pen" /> @@ -47,7 +47,7 @@ } @media all and (min-width: 801px) { - .new-status-button { + .new-status-button:not(.always-show) { display: none; } } diff --git a/src/components/moderation_tools/moderation_tools.js b/src/components/moderation_tools/moderation_tools.js index d4fdc53e3c1fcb29107b8c8828d0b91902e06ed9..2469327afea4d73fbeb9136a648b94d5690a1118 100644 --- a/src/components/moderation_tools/moderation_tools.js +++ b/src/components/moderation_tools/moderation_tools.js @@ -1,6 +1,11 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faChevronDown } from '@fortawesome/free-solid-svg-icons' + import DialogModal from '../dialog_modal/dialog_modal.vue' import Popover from '../popover/popover.vue' +library.add(faChevronDown) + const FORCE_NSFW = 'mrf_tag:media-force-nsfw' const STRIP_MEDIA = 'mrf_tag:media-strip' const FORCE_UNLISTED = 'mrf_tag:force-unlisted' diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index 5c7b82ecfa38deae705044d39022ad40a0dbd595..96476abe2136c5a968793d9eb12e66f2e5a495d8 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -8,7 +8,7 @@ @show="setToggled(true)" @close="setToggled(false)" > - <div slot="content"> + <template v-slot:content> <div class="dropdown-menu"> <span v-if="user.is_local"> <button @@ -50,96 +50,98 @@ 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> - </div> - <button - slot="trigger" - class="btn button-default btn-block" - :class="{ toggled }" - > - {{ $t('user_card.admin_menu.moderation') }} - </button> + </template> + <template v-slot:trigger> + <button + class="btn button-default btn-block moderation-tools-button" + :class="{ toggled }" + > + {{ $t('user_card.admin_menu.moderation') }} + <FAIcon icon="chevron-down" /> + </button> + </template> </Popover> <portal to="modal"> <DialogModal v-if="showDeleteUserDialog" :on-cancel="deleteUserDialog.bind(this, false)" > - <template slot="header"> + <template v-slot:header> {{ $t('user_card.admin_menu.delete_user') }} </template> <p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p> - <template slot="footer"> + <template v-slot:footer> <button class="btn button-default" @click="deleteUserDialog(false)" @@ -163,25 +165,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 { @@ -189,4 +172,10 @@ height: 100%; } } + +.moderation-tools-button { + svg,i { + font-size: 0.8em; + } +} </style> diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.js b/src/components/mrf_transparency_panel/mrf_transparency_panel.js index a0b600d2a5e66062ebaf9067e2391d1ec147d7a5..3fde8106475bb41af357f30443a87b5db59ea9d0 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.js +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.js @@ -1,17 +1,56 @@ import { mapState } from 'vuex' import { get } from 'lodash' +/** + * This is for backwards compatibility. We originally didn't recieve + * extra info like a reason why an instance was rejected/quarantined/etc. + * Because we didn't want to break backwards compatibility it was decided + * to add an extra "info" key. + */ +const toInstanceReasonObject = (instances, info, key) => { + return instances.map(instance => { + if (info[key] && info[key][instance] && info[key][instance]['reason']) { + return { instance: instance, reason: info[key][instance]['reason'] } + } + return { instance: instance, reason: '' } + }) +} + const MRFTransparencyPanel = { computed: { ...mapState({ federationPolicy: state => get(state, 'instance.federationPolicy'), mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []), - quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []), - acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []), - rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []), - ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []), - mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []), - mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', []), + quarantineInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.quarantined_instances', []), + get(state, 'instance.federationPolicy.quarantined_instances_info', []), + 'quarantined_instances' + ), + acceptInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.accept', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'accept' + ), + rejectInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.reject', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'reject' + ), + ftlRemovalInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'federated_timeline_removal' + ), + mediaNsfwInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'media_nsfw' + ), + mediaRemovalInstances: state => toInstanceReasonObject( + get(state, 'instance.federationPolicy.mrf_simple.media_removal', []), + get(state, 'instance.federationPolicy.mrf_simple_info', []), + 'media_removal' + ), keywordsFtlRemoval: state => get(state, 'instance.federationPolicy.mrf_keyword.federated_timeline_removal', []), keywordsReject: state => get(state, 'instance.federationPolicy.mrf_keyword.reject', []), keywordsReplace: state => get(state, 'instance.federationPolicy.mrf_keyword.replace', []) diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.scss b/src/components/mrf_transparency_panel/mrf_transparency_panel.scss new file mode 100644 index 0000000000000000000000000000000000000000..80ea01d47ae8d127b8433a9bed158b088b490c13 --- /dev/null +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.scss @@ -0,0 +1,21 @@ +.mrf-section { + margin: 1em; + + table { + width:100%; + text-align: left; + padding-left:10px; + padding-bottom:20px; + + th, td { + width: 180px; + max-width: 360px; + overflow: hidden; + vertical-align: text-top; + } + + th+th, td+td { + width: auto; + } + } +} diff --git a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue index acdf822e464a0dbace1057868c97c8e86cf64458..1787fa07b86806caae68c3a9e95b457dee80762f 100644 --- a/src/components/mrf_transparency_panel/mrf_transparency_panel.vue +++ b/src/components/mrf_transparency_panel/mrf_transparency_panel.vue @@ -31,13 +31,24 @@ <p>{{ $t("about.mrf.simple.accept_desc") }}</p> - <ul> - <li - v-for="instance in acceptInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in acceptInstances" + :key="entry.instance + '_accept'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="rejectInstances.length"> @@ -45,13 +56,24 @@ <p>{{ $t("about.mrf.simple.reject_desc") }}</p> - <ul> - <li - v-for="instance in rejectInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in rejectInstances" + :key="entry.instance + '_reject'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="quarantineInstances.length"> @@ -59,13 +81,24 @@ <p>{{ $t("about.mrf.simple.quarantine_desc") }}</p> - <ul> - <li - v-for="instance in quarantineInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in quarantineInstances" + :key="entry.instance + '_quarantine'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="ftlRemovalInstances.length"> @@ -73,13 +106,24 @@ <p>{{ $t("about.mrf.simple.ftl_removal_desc") }}</p> - <ul> - <li - v-for="instance in ftlRemovalInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in ftlRemovalInstances" + :key="entry.instance + '_ftl_removal'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="mediaNsfwInstances.length"> @@ -87,13 +131,24 @@ <p>{{ $t("about.mrf.simple.media_nsfw_desc") }}</p> - <ul> - <li - v-for="instance in mediaNsfwInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in mediaNsfwInstances" + :key="entry.instance + '_media_nsfw'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <div v-if="mediaRemovalInstances.length"> @@ -101,13 +156,24 @@ <p>{{ $t("about.mrf.simple.media_removal_desc") }}</p> - <ul> - <li - v-for="instance in mediaRemovalInstances" - :key="instance" - v-text="instance" - /> - </ul> + <table> + <tr> + <th>{{ $t("about.mrf.simple.instance") }}</th> + <th>{{ $t("about.mrf.simple.reason") }}</th> + </tr> + <tr + v-for="entry in mediaRemovalInstances" + :key="entry.instance + '_media_removal'" + > + <td>{{ entry.instance }}</td> + <td v-if="entry.reason === ''"> + {{ $t("about.mrf.simple.not_applicable") }} + </td> + <td v-else> + {{ entry.reason }} + </td> + </tr> + </table> </div> <h2 v-if="hasKeywordPolicies"> @@ -161,7 +227,6 @@ <script src="./mrf_transparency_panel.js"></script> <style lang="scss"> -.mrf-section { - margin: 1em; -} +@import '../../_variables.scss'; +@import './mrf_transparency_panel.scss'; </style> diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 81d49cc26016d5af7d9690058770b68a17bc911e..37bcb4095b3366a1b169459b74618f93deca0e5f 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -1,4 +1,4 @@ -import { timelineNames } from '../timeline_menu/timeline_menu.js' +import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue' import { mapState, mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' @@ -7,10 +7,12 @@ import { faGlobe, faBookmark, faEnvelope, - faHome, + faChevronDown, + faChevronUp, faComments, faBell, - faInfoCircle + faInfoCircle, + faStream } from '@fortawesome/free-solid-svg-icons' library.add( @@ -18,10 +20,12 @@ library.add( faGlobe, faBookmark, faEnvelope, - faHome, + faChevronDown, + faChevronUp, faComments, faBell, - faInfoCircle + faInfoCircle, + faStream ) const NavPanel = { @@ -30,16 +34,20 @@ const NavPanel = { this.$store.dispatch('startFetchingFollowRequests') } }, + components: { + TimelineMenuContent + }, + data () { + return { + showTimelines: false + } + }, + methods: { + toggleTimelines () { + this.showTimelines = !this.showTimelines + } + }, computed: { - onTimelineRoute () { - return !!timelineNames()[this.$route.name] - }, - timelinesRoute () { - if (this.$store.state.interface.lastTimeline) { - return this.$store.state.interface.lastTimeline - } - return this.currentUser ? 'friends' : 'public-timeline' - }, ...mapState({ currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 0c83d0fe2c5119c7085f0c36a677c0133c3fab99..7ae7b1d6eda7fb99ca09b098f66ed2c43fa01204 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -3,19 +3,33 @@ <div class="panel panel-default"> <ul> <li v-if="currentUser || !privateMode"> - <router-link - :to="{ name: timelinesRoute }" - :class="onTimelineRoute && 'router-link-active'" + <button + class="button-unstyled menu-item" + @click="toggleTimelines" > <FAIcon fixed-width class="fa-scale-110" - icon="home" + icon="stream" />{{ $t("nav.timelines") }} - </router-link> + <FAIcon + class="timelines-chevron" + fixed-width + :icon="showTimelines ? 'chevron-up' : 'chevron-down'" + /> + </button> + <div + v-show="showTimelines" + class="timelines-background" + > + <TimelineMenuContent class="timelines" /> + </div> </li> <li v-if="currentUser"> - <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> + <router-link + class="menu-item" + :to="{ name: 'interactions', params: { username: currentUser.screen_name } }" + > <FAIcon fixed-width class="fa-scale-110" @@ -24,7 +38,10 @@ </router-link> </li> <li v-if="currentUser && pleromaChatMessagesAvailable"> - <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }"> + <router-link + class="menu-item" + :to="{ name: 'chats', params: { username: currentUser.screen_name } }" + > <div v-if="unreadChatCount" class="badge badge-notification" @@ -39,7 +56,10 @@ </router-link> </li> <li v-if="currentUser && currentUser.locked"> - <router-link :to="{ name: 'friend-requests' }"> + <router-link + class="menu-item" + :to="{ name: 'friend-requests' }" + > <FAIcon fixed-width class="fa-scale-110" @@ -54,7 +74,10 @@ </router-link> </li> <li> - <router-link :to="{ name: 'about' }"> + <router-link + class="menu-item" + :to="{ name: 'about' }" + > <FAIcon fixed-width class="fa-scale-110" @@ -91,14 +114,14 @@ border-color: var(--border, $fallback--border); padding: 0; - &:first-child a { + &:first-child .menu-item { border-top-right-radius: $fallback--panelRadius; border-top-right-radius: var(--panelRadius, $fallback--panelRadius); border-top-left-radius: $fallback--panelRadius; border-top-left-radius: var(--panelRadius, $fallback--panelRadius); } - &:last-child a { + &:last-child .menu-item { border-bottom-right-radius: $fallback--panelRadius; border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); border-bottom-left-radius: $fallback--panelRadius; @@ -110,13 +133,15 @@ border: none; } - a { + .menu-item { display: block; box-sizing: border-box; - align-items: stretch; height: 3.5em; line-height: 3.5em; padding: 0 1em; + width: 100%; + color: $fallback--link; + color: var(--link, $fallback--link); &:hover { background-color: $fallback--lightBg; @@ -146,6 +171,25 @@ } } + .timelines-chevron { + margin-left: 0.8em; + font-size: 1.1em; + } + + .timelines-background { + padding: 0 0 0 0.6em; + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + border-top: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + } + + .timelines { + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + } + .fa-scale-110 { margin-right: 0.8em; } diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index 1634c0355a768374f8fe7eed070243b0c4eb4b96..8f74c0e62ee45c9a07787a493cee22f5d8cf1558 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' import Report from '../report/report.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -46,7 +47,8 @@ const Notification = { UserCard, Timeago, Status, - Report + Report, + RichContent }, methods: { toggleUserExpanded () { diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss index f59055604a40bf75589baa2c5b93af0ffee83e7c..389781371025137c064378ebebea35f3e1781a73 100644 --- a/src/components/notification/notification.scss +++ b/src/components/notification/notification.scss @@ -2,6 +2,19 @@ // TODO Copypaste from Status, should unify it somehow .Notification { + border-bottom: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + word-wrap: break-word; + word-break: break-word; + --emoji-size: 14px; + + &:hover { + --_still-image-img-visibility: visible; + --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; + } + &.-muted { padding: 0.25em 0.6em; height: 1.2em; diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 40ea04312e35a74094e14270c770ca79f8f17f25..e10eff00bd97ecceaa07372fdd62e956a56ac1d7 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -1,6 +1,7 @@ <template> <Status v-if="notification.type === 'mention'" + class="Notification" :compact="true" :statusoid="notification.status" /> @@ -11,7 +12,7 @@ > <small> <router-link :to="userProfileLink"> - {{ notification.from_profile.screen_name }} + {{ notification.from_profile.screen_name_ui }} </router-link> </small> <button @@ -51,17 +52,19 @@ <span class="notification-details"> <div class="name-and-action"> <!-- eslint-disable vue/no-v-html --> - <bdi - v-if="!!notification.from_profile.name_html" - class="username" - :title="'@'+notification.from_profile.screen_name" - v-html="notification.from_profile.name_html" - /> + <bdi v-if="!!notification.from_profile.name_html"> + <RichContent + class="username" + :title="'@'+notification.from_profile.screen_name_ui" + :html="notification.from_profile.name_html" + :emoji="notification.from_profile.emoji" + /> + </bdi> <!-- eslint-enable vue/no-v-html --> <span v-else 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 @@ -155,7 +158,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'" @@ -180,7 +183,7 @@ class="move-text" > <router-link :to="targetUserProfileLink"> - @{{ notification.target.screen_name }} + @{{ notification.target.screen_name_ui }} </router-link> </div> <Report @@ -188,8 +191,9 @@ :report-id="notification.report.id" /> <template v-else> - <status-content + <StatusContent class="faint" + :compact="true" :status="notification.action" /> </template> diff --git a/src/components/notifications/notification_filters.vue b/src/components/notifications/notification_filters.vue new file mode 100644 index 0000000000000000000000000000000000000000..ba0e90a062ae262338f605a4c6985cec6d59c21f --- /dev/null +++ b/src/components/notifications/notification_filters.vue @@ -0,0 +1,122 @@ +<template> + <Popover + trigger="click" + class="NotificationFilters" + placement="bottom" + :bound-to="{ x: 'container' }" + > + <template v-slot:content> + <div class="dropdown-menu"> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('likes')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.likes }" + />{{ $t('settings.notification_visibility_likes') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('repeats')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.repeats }" + />{{ $t('settings.notification_visibility_repeats') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('follows')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.follows }" + />{{ $t('settings.notification_visibility_follows') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('mentions')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.mentions }" + />{{ $t('settings.notification_visibility_mentions') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('emojiReactions')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.emojiReactions }" + />{{ $t('settings.notification_visibility_emoji_reactions') }} + </button> + <button + class="button-default dropdown-item" + @click="toggleNotificationFilter('moves')" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': filters.moves }" + />{{ $t('settings.notification_visibility_moves') }} + </button> + </div> + </template> + <template v-slot:trigger> + <button class="button-unstyled"> + <FAIcon icon="filter" /> + </button> + </template> + </Popover> +</template> + +<script> +import Popover from '../popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faFilter } from '@fortawesome/free-solid-svg-icons' + +library.add( + faFilter +) + +export default { + components: { Popover }, + computed: { + filters () { + return this.$store.getters.mergedConfig.notificationVisibility + } + }, + methods: { + toggleNotificationFilter (type) { + this.$store.dispatch('setOption', { + name: 'notificationVisibility', + value: { + ...this.filters, + [type]: !this.filters[type] + } + }) + } + } +} +</script> + +<style lang="scss"> + +.NotificationFilters { + 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; + } +} + +</style> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index 492585631d28599049d62bbb4c9a13a2a375b3dd..c8f1ebcbe2957192eac1c18a98f68e7aade11fed 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -1,5 +1,6 @@ import { mapGetters } from 'vuex' import Notification from '../notification/notification.vue' +import NotificationFilters from './notification_filters.vue' import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js' import { notificationsFromStore, @@ -17,6 +18,10 @@ library.add( const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 const Notifications = { + components: { + Notification, + NotificationFilters + }, props: { // Disables display of panel header noHeading: Boolean, @@ -35,11 +40,6 @@ const Notifications = { seenToDisplayCount: DEFAULT_SEEN_TO_DISPLAY_COUNT } }, - created () { - const store = this.$store - const credentials = store.state.users.currentUser.credentials - notificationsFetcher.fetchAndUpdate({ store, credentials }) - }, computed: { mainClass () { return this.minimalMode ? '' : 'panel panel-default' @@ -70,9 +70,6 @@ const Notifications = { }, ...mapGetters(['unreadChatCount']) }, - components: { - Notification - }, watch: { unseenCountTitle (count) { if (count > 0) { diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index 58a86fb09a71325d1e91b632ec4b9c484d1a0bd9..47c035f175d6457ab293c8b991223a8d3f8939da 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -1,6 +1,6 @@ @import '../../_variables.scss'; -.notifications { +.Notifications { &:not(.minimal) { // a bit of a hack to allow scrolling below notifications padding-bottom: 15em; @@ -11,6 +11,10 @@ color: var(--text, $fallback--text); } + .notifications-footer { + border: none; + } + .notification { position: relative; @@ -33,11 +37,6 @@ .notification { box-sizing: border-box; - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - word-wrap: break-word; - word-break: break-word; &:hover .animated.Avatar { canvas { @@ -84,7 +83,6 @@ } } - .follow-text, .move-text { padding: 0.5em 0; overflow-wrap: break-word; @@ -147,13 +145,6 @@ max-width: 100%; text-overflow: ellipsis; white-space: nowrap; - - img { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain - } } .timeago { diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index 725d1ad4edafc09be4861f6ea90f2c418f99d8bc..2ce5d56f7e3a4ae1c664a70130d6dee1c64a67f3 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -1,7 +1,7 @@ <template> <div :class="{ minimal: minimalMode }" - class="notifications" + class="Notifications" > <div :class="mainClass"> <div @@ -22,6 +22,7 @@ > {{ $t('notifications.read') }} </button> + <NotificationFilters /> </div> <div class="panel-body"> <div @@ -34,10 +35,10 @@ <notification :notification="notification" /> </div> </div> - <div class="panel-footer"> + <div class="panel-footer notifications-footer"> <div v-if="bottomedOut" - class="new-status-notification text-center panel-footer faint" + class="new-status-notification text-center faint" > {{ $t('notifications.no_more_notifications') }} </div> @@ -46,13 +47,13 @@ class="button-unstyled -link -fullwidth" @click.prevent="fetchOlderNotifications()" > - <div class="new-status-notification text-center panel-footer"> + <div class="new-status-notification text-center"> {{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }} </div> </button> <div v-else - class="new-status-notification text-center panel-footer" + class="new-status-notification text-center" > <FAIcon icon="circle-notch" diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue index a931cb5aedfb8a550cac3f84ebf2d8f629dd6886..3ffa5425cdfc01adb12defa81109aac759eaf6c9 100644 --- a/src/components/password_reset/password_reset.vue +++ b/src/components/password_reset/password_reset.vue @@ -53,7 +53,7 @@ type="submit" class="btn button-default btn-block" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> </div> diff --git a/src/components/poll/poll.js b/src/components/poll/poll.js index 98db5582eb841c0608c85ee830603db435546fa7..a69b78861b9c54e8cd576d80f7ea0f15dae6d0e7 100644 --- a/src/components/poll/poll.js +++ b/src/components/poll/poll.js @@ -1,10 +1,14 @@ -import Timeago from '../timeago/timeago.vue' +import Timeago from 'components/timeago/timeago.vue' +import RichContent from 'components/rich_content/rich_content.jsx' import { forEach, map } from 'lodash' export default { name: 'Poll', - props: ['basePoll'], - components: { Timeago }, + props: ['basePoll', 'emoji'], + components: { + Timeago, + RichContent + }, data () { return { loading: false, diff --git a/src/components/poll/poll.vue b/src/components/poll/poll.vue index 42819c19faca04f250350bb1d0f20699d96b12d4..63b44e4facf083e09418a2bd1bb7f0aa8779b319 100644 --- a/src/components/poll/poll.vue +++ b/src/components/poll/poll.vue @@ -17,8 +17,11 @@ <span class="result-percentage"> {{ percentageForOption(option.votes_count) }}% </span> - <!-- eslint-disable-next-line vue/no-v-html --> - <span v-html="option.title_html" /> + <RichContent + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> </div> <div class="result-fill" @@ -42,8 +45,11 @@ :value="index" > <label class="option-vote"> - <!-- eslint-disable-next-line vue/no-v-html --> - <div v-html="option.title_html" /> + <RichContent + :html="option.title_html" + :handle-links="false" + :emoji="emoji" + /> </label> </div> </div> @@ -58,7 +64,12 @@ {{ $t('polls.vote') }} </button> <div class="total"> - {{ totalVotesCount }} {{ $t("polls.votes") }} · + <template v-if="typeof poll.voters_count === 'number'"> + {{ $tc("polls.people_voted_count", poll.voters_count, { count: poll.voters_count }) }} · + </template> + <template v-else> + {{ $tc("polls.votes_count", poll.votes_count, { count: poll.votes_count }) }} · + </template> </div> <i18n :path="expired ? 'polls.expired' : 'polls.expires_in'"> <Timeago diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js index 1f8df3f94cfdd3f9162dee5dbeddd66c9fe8b8d6..e30645c34e865796291cef2877e42ba63130f278 100644 --- a/src/components/poll/poll_form.js +++ b/src/components/poll/poll_form.js @@ -1,19 +1,21 @@ import * as DateUtils from 'src/services/date_utils/date_utils.js' import { uniq } from 'lodash' import { library } from '@fortawesome/fontawesome-svg-core' +import Select from '../select/select.vue' import { faTimes, - faChevronDown, faPlus } from '@fortawesome/free-solid-svg-icons' library.add( faTimes, - faChevronDown, faPlus ) export default { + components: { + Select + }, name: 'PollForm', props: ['visible'], data: () => ({ diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue index 094961052a2fc98f611199e2fb7b2dfa275075f0..3620075ab9fb55dd304afff2da69d7ede314a109 100644 --- a/src/components/poll/poll_form.vue +++ b/src/components/poll/poll_form.vue @@ -46,23 +46,19 @@ class="poll-type" :title="$t('polls.type')" > - <label - for="poll-type-selector" - class="select" + <Select + v-model="pollType" + class="poll-type-select" + unstyled="true" + @change="updatePollToParent" > - <select - v-model="pollType" - class="select" - @change="updatePollToParent" - > - <option value="single">{{ $t('polls.single_choice') }}</option> - <option value="multiple">{{ $t('polls.multiple_choices') }}</option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + <option value="single"> + {{ $t('polls.single_choice') }} + </option> + <option value="multiple"> + {{ $t('polls.multiple_choices') }} + </option> + </Select> </div> <div class="poll-expiry" @@ -76,24 +72,20 @@ :max="maxExpirationInCurrentUnit" @change="expiryAmountChange" > - <label class="expiry-unit select"> - <select - v-model="expiryUnit" - @change="expiryAmountChange" + <Select + v-model="expiryUnit" + unstyled="true" + class="expiry-unit" + @change="expiryAmountChange" + > + <option + v-for="unit in expiryUnits" + :key="unit" + :value="unit" > - <option - v-for="unit in expiryUnits" - :key="unit" - :value="unit" - > - {{ $t(`time.${unit}_short`, ['']) }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ $t(`time.${unit}_short`, ['']) }} + </option> + </Select> </div> </div> </div> @@ -147,10 +139,9 @@ .poll-type { margin-right: 0.75em; flex: 1 1 60%; - .select { - border: none; - box-shadow: none; - background-color: transparent; + + .poll-type-select { + padding-right: 0.75em; } } @@ -161,12 +152,6 @@ width: 3em; text-align: right; } - - .expiry-unit { - border: none; - box-shadow: none; - background-color: transparent; - } } } </style> diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js index 5e417fa0ed335aa8822622f62970a02593cefb1a..6ccf32f0575dc3a9ebd539ea6e1dd776fdc33680 100644 --- 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 @@ -47,8 +54,11 @@ const Popover = { } // Popover will be anchored around this element, trigger ref is the container, so - // its children are what are inside the slot. Expect only one slot="trigger". + // its children are what are inside the slot. Expect only one v-slot:trigger. const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el + // SVGs don't have offsetWidth/Height, use fallback + const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth + const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight const screenBox = anchorEl.getBoundingClientRect() // Screen position of the origin point for popover const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top } @@ -107,11 +117,11 @@ const Popover = { const yOffset = (this.offset && this.offset.y) || 0 const translateY = usingTop - ? -anchorEl.offsetHeight + vPadding - yOffset - content.offsetHeight + ? -anchorHeight + vPadding - yOffset - content.offsetHeight : yOffset const xOffset = (this.offset && this.offset.x) || 0 - const translateX = (anchorEl.offsetWidth * 0.5) - content.offsetWidth * 0.5 + horizOffset + xOffset + const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset // Note, separate translateX and translateY avoids blurry text on chromium, // single translate or translate3d resulted in blurry text. @@ -121,9 +131,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 index 2252c68f37e89687b523b71ae675a290c6f77550..8588b35150052fbfad93051b50aee9b913a5b33a 100644 --- 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" /> @@ -32,7 +33,7 @@ @import '../../_variables.scss'; .popover-trigger-button { - display: block; + display: inline-block; } .popover { @@ -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 index 4148381c830b5fbaba24750e166eaf1f08fb0faf..fe07309fc9f3a23bdace0aedb7fda085c2cda45f 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -4,6 +4,7 @@ import ScopeSelector from '../scope_selector/scope_selector.vue' import EmojiInput from '../emoji_input/emoji_input.vue' import PollForm from '../poll/poll_form.vue' import Attachment from '../attachment/attachment.vue' +import Gallery from 'src/components/gallery/gallery.vue' import StatusContent from '../status_content/status_content.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' @@ -11,10 +12,10 @@ import { reject, map, uniqBy, debounce } from 'lodash' import suggestor from '../emoji_input/suggestor.js' import { mapGetters, mapState } from 'vuex' import Checkbox from '../checkbox/checkbox.vue' +import Select from '../select/select.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { - faChevronDown, faSmileBeam, faPollH, faUpload, @@ -24,7 +25,6 @@ import { } from '@fortawesome/free-solid-svg-icons' library.add( - faChevronDown, faSmileBeam, faPollH, faUpload, @@ -84,8 +84,10 @@ const PostStatusForm = { PollForm, ScopeSelector, Checkbox, + Select, Attachment, - StatusContent + StatusContent, + Gallery }, mounted () { this.updateIdempotencyKey() @@ -115,7 +117,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 +128,7 @@ const PostStatusForm = { newStatus: { spoilerText: this.subject || '', status: statusText, - nsfw: false, + nsfw: !!sensitiveByDefault, files: [], poll: {}, mediaDescriptions: {}, @@ -388,6 +390,21 @@ const PostStatusForm = { this.newStatus.files.splice(index, 1) this.$emit('resize') }, + editAttachment (fileInfo, newText) { + this.newStatus.mediaDescriptions[fileInfo.id] = newText + }, + shiftUpMediaFile (fileInfo) { + const { files } = this.newStatus + const index = this.newStatus.files.indexOf(fileInfo) + files.splice(index, 1) + files.splice(index - 1, 0, fileInfo) + }, + shiftDnMediaFile (fileInfo) { + const { files } = this.newStatus + const index = this.newStatus.files.indexOf(fileInfo) + files.splice(index, 1) + files.splice(index + 1, 0, fileInfo) + }, uploadFailed (errString, templateArgs) { templateArgs = templateArgs || {} this.error = this.$t('upload.error.base') + ' ' + this.$t('upload.error.' + errString, templateArgs) diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 73f6a4f10ead987caa00152130bf98d638b66af9..2e0980a256bb6a6cac8a5a654c4fcf6b6672c28d 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -189,28 +189,19 @@ v-if="postFormats.length > 1" class="text-format" > - <label - for="post-content-type" - class="select" + <Select + id="post-content-type" + v-model="newStatus.contentType" + class="form-control" > - <select - id="post-content-type" - v-model="newStatus.contentType" - class="form-control" + <option + v-for="postFormat in postFormats" + :key="postFormat" + :value="postFormat" > - <option - v-for="postFormat in postFormats" - :key="postFormat" - :value="postFormat" - > - {{ $t(`post_status.content_type["${postFormat}"]`) }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ $t(`post_status.content_type["${postFormat}"]`) }} + </option> + </Select> </div> <div v-if="postFormats.length === 1 && postFormats[0] !== 'text/plain'" @@ -272,7 +263,7 @@ disabled class="btn button-default" > - {{ $t('general.submit') }} + {{ $t('post_status.post') }} </button> <!-- touchstart is used to keep the OSK at the same position after a message send --> <button @@ -282,7 +273,7 @@ @touchstart.stop.prevent="postStatus($event, newStatus)" @click.stop.prevent="postStatus($event, newStatus)" > - {{ $t('general.submit') }} + {{ $t('post_status.post') }} </button> </div> <div @@ -296,32 +287,22 @@ @click="clearError" /> </div> - <div class="attachments"> - <div - v-for="file in newStatus.files" - :key="file.url" - class="media-upload-wrapper" - > - <button - class="button-unstyled hider" - @click="removeMediaFile(file)" - > - <FAIcon icon="times" /> - </button> - <attachment - :attachment="file" - :set-media="() => $store.dispatch('setMedia', newStatus.files)" - size="small" - allow-play="false" - /> - <input - v-model="newStatus.mediaDescriptions[file.id]" - type="text" - :placeholder="$t('post_status.media_description')" - @keydown.enter.prevent="" - > - </div> - </div> + <gallery + v-if="newStatus.files && newStatus.files.length > 0" + class="attachments" + :grid="true" + :nsfw="false" + :attachments="newStatus.files" + :descriptions="newStatus.mediaDescriptions" + :set-media="() => $store.dispatch('setMedia', newStatus.files)" + :editable="true" + :edit-attachment="editAttachment" + :remove-attachment="removeMediaFile" + :shift-up-attachment="newStatus.files.length > 1 && shiftUpMediaFile" + :shift-dn-attachment="newStatus.files.length > 1 && shiftDnMediaFile" + @play="$emit('mediaplay', attachment.id)" + @pause="$emit('mediapause', attachment.id)" + /> <div v-if="newStatus.files.length > 0 && !disableSensitivityCheckbox" class="upload_settings" @@ -339,26 +320,13 @@ <style lang="scss"> @import '../../_variables.scss'; -.tribute-container { - ul { - padding: 0px; - li { - display: flex; - align-items: center; - } - } - img { - padding: 3px; - width: 16px; - height: 16px; - border-radius: $fallback--avatarAltRadius; - border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); - } -} - .post-status-form { position: relative; + .attachments { + margin-bottom: 0.5em; + } + .form-bottom { display: flex; justify-content: space-between; @@ -516,15 +484,6 @@ flex-direction: column; } - .attachments .media-upload-wrapper { - position: relative; - - .attachment { - margin: 0; - padding: 0; - } - } - .btn { cursor: pointer; } @@ -625,11 +584,4 @@ border: 2px dashed var(--text, $fallback--text); } } - -// todo: unify with attachment.vue (otherwise the uploaded images are not minified unless a status with an attachment was displayed before) -img.media-upload, .media-upload-container > video { - line-height: 0; - max-height: 200px; - max-width: 100%; -} </style> diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js index 5e7b7580b149671d8d35be6975aaa4703cc8938c..ce82c90da7378bd99dc2e5a2d8028a08946f0cdc 100644 --- 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 index ac940b9890e2f05a89e0955d5fbdb07d68aedf59..c69c315ba7d0069959e85a18724e002c83d90c6d 100644 --- a/src/components/react_button/react_button.vue +++ b/src/components/react_button/react_button.vue @@ -1,15 +1,14 @@ <template> <Popover trigger="click" + class="ReactButton" placement="top" :offset="{ y: 5 }" :bound-to="{ x: 'container' }" remove-padding + @show="focusInput" > - <div - slot="content" - slot-scope="{close}" - > + <template v-slot:content="{close}"> <div class="reaction-picker-filter"> <input v-model="filterWord" @@ -39,17 +38,18 @@ </span> <div class="reaction-bottom-fader" /> </div> - </div> - <span - slot="trigger" - class="ReactButton" - :title="$t('tool_tip.add_reaction')" - > - <FAIcon - class="fa-scale-110 fa-old-padding" - :icon="['far', 'smile-beam']" - /> - </span> + </template> + <template v-slot:trigger> + <button + class="button-unstyled popover-trigger" + :title="$t('tool_tip.add_reaction')" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + :icon="['far', 'smile-beam']" + /> + </button> + </template> </Popover> </template> @@ -58,62 +58,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 index dab06e1ee49401bf5f1addf2dde788ec868709fb..1ac8e8beab8eaa9a06b423d8c3c8690dcf42f1fe 100644 --- 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 index 100df0d6e576f05f2f538af322807a28474ce52d..65b4bb33c9b6b0ec383fb4552b24f266c5b9c35a 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -162,6 +162,23 @@ </ul> </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" @@ -213,7 +230,7 @@ type="submit" class="btn button-default" > - {{ $t('general.submit') }} + {{ $t('registration.register') }} </button> </div> </div> diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx new file mode 100644 index 0000000000000000000000000000000000000000..46bc661afbb6f2faa7713e9c142247391bb3caa0 --- /dev/null +++ b/src/components/rich_content/rich_content.jsx @@ -0,0 +1,328 @@ +import Vue from 'vue' +import { unescape, flattenDeep } from 'lodash' +import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js' +import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js' +import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js' +import StillImage from 'src/components/still-image/still-image.vue' +import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue' +import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue' + +import './rich_content.scss' + +/** + * RichContent, The Ãœber-powered component for rendering Post HTML. + * + * This takes post HTML and does multiple things to it: + * - Groups all mentions into <MentionsLine>, this affects all mentions regardles + * of where they are (beginning/middle/end), even single mentions are converted + * to a <MentionsLine> containing single <MentionLink>. + * - Replaces emoji shortcodes with <StillImage>'d images. + * + * There are two problems with this component's architecture: + * 1. Parsing HTML and rendering are inseparable. Attempts to separate the two + * proven to be a massive overcomplication due to amount of things done here. + * 2. We need to output both render and some extra data, which seems to be imp- + * possible in vue. Current solution is to emit 'parseReady' event when parsing + * is done within render() function. + * + * Apart from that one small hiccup with emit in render this _should_ be vue3-ready + */ +export default Vue.component('RichContent', { + name: 'RichContent', + props: { + // Original html content + html: { + required: true, + type: String + }, + attentions: { + required: false, + default: () => [] + }, + // Emoji object, as in status.emojis, note the "s" at the end... + emoji: { + required: true, + type: Array + }, + // Whether to handle links or not (posts: yes, everything else: no) + handleLinks: { + required: false, + type: Boolean, + default: false + }, + // Meme arrows + greentext: { + required: false, + type: Boolean, + default: false + } + }, + // NEVER EVER TOUCH DATA INSIDE RENDER + render (h) { + // Pre-process HTML + const { newHtml: html } = preProcessPerLine(this.html, this.greentext) + let currentMentions = null // Current chain of mentions, we group all mentions together + // This is used to recover spacing removed when parsing mentions + let lastSpacing = '' + + const lastTags = [] // Tags that appear at the end of post body + const writtenMentions = [] // All mentions that appear in post body + const invisibleMentions = [] // All mentions that go beyond the limiter (see MentionsLine) + // to collapse too many mentions in a row + const writtenTags = [] // All tags that appear in post body + // unique index for vue "tag" property + let mentionIndex = 0 + let tagsIndex = 0 + + const renderImage = (tag) => { + return <StillImage + {...{ attrs: getAttrs(tag) }} + class="img" + /> + } + + const renderHashtag = (attrs, children, encounteredTextReverse) => { + const linkData = getLinkData(attrs, children, tagsIndex++) + writtenTags.push(linkData) + if (!encounteredTextReverse) { + lastTags.push(linkData) + } + return <HashtagLink {...{ props: linkData }}/> + } + + const renderMention = (attrs, children) => { + const linkData = getLinkData(attrs, children, mentionIndex++) + linkData.notifying = this.attentions.some(a => a.statusnet_profile_url === linkData.url) + writtenMentions.push(linkData) + if (currentMentions === null) { + currentMentions = [] + } + currentMentions.push(linkData) + if (currentMentions.length > MENTIONS_LIMIT) { + invisibleMentions.push(linkData) + } + if (currentMentions.length === 1) { + return <MentionsLine mentions={ currentMentions } /> + } else { + return '' + } + } + + // Processor to use with html_tree_converter + const processItem = (item, index, array, what) => { + // Handle text nodes - just add emoji + if (typeof item === 'string') { + const emptyText = item.trim() === '' + if (item.includes('\n')) { + currentMentions = null + } + if (emptyText) { + // don't include spaces when processing mentions - we'll include them + // in MentionsLine + lastSpacing = item + // Don't remove last space in a container (fixes poast mentions) + return (index !== array.length - 1) && (currentMentions !== null) ? item.trim() : item + } + + currentMentions = null + if (item.includes(':')) { + item = ['', processTextForEmoji( + item, + this.emoji, + ({ shortcode, url }) => { + return <StillImage + class="emoji img" + src={url} + title={`:${shortcode}:`} + alt={`:${shortcode}:`} + /> + } + )] + } + return item + } + + // Handle tag nodes + if (Array.isArray(item)) { + const [opener, children, closer] = item + const Tag = getTagName(opener) + const attrs = getAttrs(opener) + const previouslyMentions = currentMentions !== null + /* During grouping of mentions we trim all the empty text elements + * This padding is added to recover last space removed in case + * we have a tag right next to mentions + */ + const mentionsLinePadding = + // Padding is only needed if we just finished parsing mentions + previouslyMentions && + // Don't add padding if content is string and has padding already + !(children && typeof children[0] === 'string' && children[0].match(/^\s/)) + ? lastSpacing + : '' + switch (Tag) { + case 'br': + currentMentions = null + break + case 'img': // replace images with StillImage + return ['', [mentionsLinePadding, renderImage(opener)], ''] + case 'a': // replace mentions with MentionLink + if (!this.handleLinks) break + if (attrs['class'] && attrs['class'].includes('mention')) { + // Handling mentions here + return renderMention(attrs, children) + } else { + currentMentions = null + break + } + case 'span': + if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) { + return ['', children.map(processItem), ''] + } + } + + if (children !== undefined) { + return [ + '', + [ + mentionsLinePadding, + [opener, children.map(processItem), closer] + ], + '' + ] + } else { + return ['', [mentionsLinePadding, item], ''] + } + } + } + + // Processor for back direction (for finding "last" stuff, just easier this way) + let encounteredTextReverse = false + const processItemReverse = (item, index, array, what) => { + // Handle text nodes - just add emoji + if (typeof item === 'string') { + const emptyText = item.trim() === '' + if (emptyText) return item + if (!encounteredTextReverse) encounteredTextReverse = true + return unescape(item) + } else if (Array.isArray(item)) { + // Handle tag nodes + const [opener, children] = item + const Tag = opener === '' ? '' : getTagName(opener) + switch (Tag) { + case 'a': // replace mentions with MentionLink + if (!this.handleLinks) break + const attrs = getAttrs(opener) + // should only be this + if ( + (attrs['class'] && attrs['class'].includes('hashtag')) || // Pleroma style + (attrs['rel'] === 'tag') // Mastodon style + ) { + return renderHashtag(attrs, children, encounteredTextReverse) + } else { + attrs.target = '_blank' + const newChildren = [...children].reverse().map(processItemReverse).reverse() + + return <a {...{ attrs }}> + { newChildren } + </a> + } + case '': + return [...children].reverse().map(processItemReverse).reverse() + } + + // Render tag as is + if (children !== undefined) { + const newChildren = Array.isArray(children) + ? [...children].reverse().map(processItemReverse).reverse() + : children + return <Tag {...{ attrs: getAttrs(opener) }}> + { newChildren } + </Tag> + } else { + return <Tag/> + } + } + return item + } + + const pass1 = convertHtmlToTree(html).map(processItem) + const pass2 = [...pass1].reverse().map(processItemReverse).reverse() + // DO NOT USE SLOTS they cause a re-render feedback loop here. + // slots updated -> rerender -> emit -> update up the tree -> rerender -> ... + // at least until vue3? + const result = <span class="RichContent"> + { pass2 } + </span> + + const event = { + lastTags, + writtenMentions, + writtenTags, + invisibleMentions + } + + // DO NOT MOVE TO UPDATE. BAD IDEA. + this.$emit('parseReady', event) + + return result + } +}) + +const getLinkData = (attrs, children, index) => { + const stripTags = (item) => { + if (typeof item === 'string') { + return item + } else { + return item[1].map(stripTags).join('') + } + } + const textContent = children.map(stripTags).join('') + return { + index, + url: attrs.href, + tag: attrs['data-tag'], + content: flattenDeep(children).join(''), + textContent + } +} + +/** Pre-processing HTML + * + * Currently this does one thing: + * - add green/cyantexting + * + * @param {String} html - raw HTML to process + * @param {Boolean} greentext - whether to enable greentexting or not + */ +export const preProcessPerLine = (html, greentext) => { + const greentextHandle = new Set(['p', 'div']) + + const lines = convertHtmlToLines(html) + const newHtml = lines.reverse().map((item, index, array) => { + if (!item.text) return item + const string = item.text + + // Greentext stuff + if ( + // Only if greentext is engaged + greentext && + // Only handle p's and divs. Don't want to affect blockquotes, code etc + item.level.every(l => greentextHandle.has(l)) && + // Only if line begins with '>' or '<' + (string.includes('>') || string.includes('<')) + ) { + const cleanedString = string.replace(/<[^>]+?>/gi, '') // remove all tags + .replace(/@\w+/gi, '') // remove mentions (even failed ones) + .trim() + if (cleanedString.startsWith('>')) { + return `<span class='greentext'>${string}</span>` + } else if (cleanedString.startsWith('<')) { + return `<span class='cyantext'>${string}</span>` + } + } + + return string + }).reverse().join('') + + return { newHtml } +} diff --git a/src/components/rich_content/rich_content.scss b/src/components/rich_content/rich_content.scss new file mode 100644 index 0000000000000000000000000000000000000000..db08ef1eac7be625e7edf63b24468d58cb9a7306 --- /dev/null +++ b/src/components/rich_content/rich_content.scss @@ -0,0 +1,64 @@ +.RichContent { + blockquote { + margin: 0.2em 0 0.2em 2em; + font-style: italic; + } + + pre { + overflow: auto; + } + + code, + samp, + kbd, + var, + pre { + font-family: var(--postCodeFont, monospace); + } + + p { + margin: 0 0 1em 0; + } + + p:last-child { + margin: 0 0 0 0; + } + + h1 { + font-size: 1.1em; + line-height: 1.2em; + margin: 1.4em 0; + } + + h2 { + font-size: 1.1em; + margin: 1em 0; + } + + h3 { + font-size: 1em; + margin: 1.2em 0; + } + + h4 { + margin: 1.1em 0; + } + + .img { + display: inline-block; + } + + .emoji { + display: inline-block; + width: var(--emoji-size, 32px); + height: var(--emoji-size, 32px); + } + + .img, + video { + max-width: 100%; + max-height: 400px; + vertical-align: middle; + object-fit: contain; + } +} diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue index 66ac612ea77198ad4e125762b0a9e6aae151573e..a01242fc1c41b16d6edbc2735d2fbdf35fc013bf 100644 --- 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 index a6503c9f74484678f4104e4373550a0a7686b8e7..b7bfc1f3957e94e50665bb75c6bb38451b589b2d 100644 --- 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 index 6cf9179e05715ed04a9116d8aed35b9bbb2855c1..222f57ba334db8a355feb8ccc58b92202394c1ab 100644 --- 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/select/select.js b/src/components/select/select.js new file mode 100644 index 0000000000000000000000000000000000000000..49535d0799a9fb3a19f8eecf682c089ea7022570 --- /dev/null +++ b/src/components/select/select.js @@ -0,0 +1,21 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown +) + +export default { + model: { + prop: 'value', + event: 'change' + }, + props: [ + 'value', + 'disabled', + 'unstyled', + 'kind' + ] +} diff --git a/src/components/select/select.vue b/src/components/select/select.vue new file mode 100644 index 0000000000000000000000000000000000000000..5ade1fa644c076bd0211ae8004a3ecf05c53fd9a --- /dev/null +++ b/src/components/select/select.vue @@ -0,0 +1,62 @@ + +<template> + <label + class="Select input" + :class="{ disabled, unstyled }" + > + <select + :disabled="disabled" + :value="value" + @change="$emit('change', $event.target.value)" + > + <slot /> + </select> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> + </label> +</template> + +<script src="./select.js"> </script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.Select { + padding: 0; + + select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: transparent; + border: none; + color: $fallback--text; + color: var(--inputText, --text, $fallback--text); + margin: 0; + padding: 0 2em 0 .2em; + font-family: sans-serif; + font-family: var(--inputFont, sans-serif); + font-size: 14px; + width: 100%; + z-index: 1; + height: 28px; + line-height: 16px; + } + + .select-down-icon { + position: absolute; + top: 0; + bottom: 0; + right: 5px; + height: 100%; + color: $fallback--text; + color: var(--inputText, $fallback--text); + line-height: 28px; + z-index: 0; + pointer-events: none; + } + +} +</style> diff --git a/src/components/selectable_list/selectable_list.vue b/src/components/selectable_list/selectable_list.vue index a9bb12a187e9c048e9a0dd4719eb2e617348c6e2..3f8858818a87175168157535dbb585c863a4379a 100644 --- a/src/components/selectable_list/selectable_list.vue +++ b/src/components/selectable_list/selectable_list.vue @@ -24,10 +24,7 @@ :items="items" :get-key="getKey" > - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <div class="selectable-list-item-inner" :class="{ 'selectable-list-item-selected-inner': isSelected(item) }" @@ -44,7 +41,7 @@ /> </div> </template> - <template slot="empty"> + <template v-slot:empty> <slot name="empty" /> </template> </List> diff --git a/src/components/settings_modal/helpers/boolean_setting.js b/src/components/settings_modal/helpers/boolean_setting.js new file mode 100644 index 0000000000000000000000000000000000000000..5c52f697cea1952288b381fbcc3e40e34c0ab735 --- /dev/null +++ b/src/components/settings_modal/helpers/boolean_setting.js @@ -0,0 +1,38 @@ +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 () { + const value = get(this.$parent, this.path) + if (value === undefined) { + return this.defaultState + } else { + return value + } + }, + defaultState () { + return get(this.$parent, this.pathDefault) + }, + isChanged () { + return this.state !== this.defaultState + } + }, + methods: { + update (e) { + set(this.$parent, this.path, e) + } + } +} diff --git a/src/components/settings_modal/helpers/boolean_setting.vue b/src/components/settings_modal/helpers/boolean_setting.vue new file mode 100644 index 0000000000000000000000000000000000000000..c3ee6583f6fb8d8a6520d095ea14a296f76e147b --- /dev/null +++ b/src/components/settings_modal/helpers/boolean_setting.vue @@ -0,0 +1,21 @@ +<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 src="./boolean_setting.js"></script> diff --git a/src/components/settings_modal/helpers/choice_setting.js b/src/components/settings_modal/helpers/choice_setting.js new file mode 100644 index 0000000000000000000000000000000000000000..a15f6bac0b466d7411d0f8df2dafcf8e9e88ccd2 --- /dev/null +++ b/src/components/settings_modal/helpers/choice_setting.js @@ -0,0 +1,39 @@ +import { get, set } from 'lodash' +import Select from 'src/components/select/select.vue' +import ModifiedIndicator from './modified_indicator.vue' +export default { + components: { + Select, + ModifiedIndicator + }, + props: [ + 'path', + 'disabled', + 'options' + ], + computed: { + pathDefault () { + const [firstSegment, ...rest] = this.path.split('.') + return [firstSegment + 'DefaultValue', ...rest].join('.') + }, + state () { + const value = get(this.$parent, this.path) + if (value === undefined) { + return this.defaultState + } else { + return value + } + }, + defaultState () { + return get(this.$parent, this.pathDefault) + }, + isChanged () { + return this.state !== this.defaultState + } + }, + methods: { + update (e) { + set(this.$parent, this.path, e) + } + } +} diff --git a/src/components/settings_modal/helpers/choice_setting.vue b/src/components/settings_modal/helpers/choice_setting.vue new file mode 100644 index 0000000000000000000000000000000000000000..fa17661bdd552f0f3fe594fde06f1b58c28dc8fa --- /dev/null +++ b/src/components/settings_modal/helpers/choice_setting.vue @@ -0,0 +1,29 @@ +<template> + <label + class="ChoiceSetting" + > + <slot /> + <Select + :value="state" + :disabled="disabled" + @change="update" + > + <option + v-for="option in options" + :key="option.key" + :value="option.value" + > + {{ option.label }} + {{ option.value === defaultState ? $t('settings.instance_default_simple') : '' }} + </option> + </Select> + <ModifiedIndicator :changed="isChanged" /> + </label> +</template> + +<script src="./choice_setting.js"></script> + +<style lang="scss"> +.ChoiceSetting { +} +</style> diff --git a/src/components/settings_modal/helpers/modified_indicator.vue b/src/components/settings_modal/helpers/modified_indicator.vue new file mode 100644 index 0000000000000000000000000000000000000000..ad212db963258cce420440f2e5ce8f2805dbbd5d --- /dev/null +++ b/src/components/settings_modal/helpers/modified_indicator.vue @@ -0,0 +1,51 @@ +<template> + <span + v-if="changed" + class="ModifiedIndicator" + > + <Popover + trigger="hover" + > + <template v-slot:trigger> + + <FAIcon + icon="wrench" + :aria-label="$t('settings.setting_changed')" + /> + </template> + <template v-slot:content> + <div class="modified-tooltip"> + {{ $t('settings.setting_changed') }} + </div> + </template> + </Popover> + </span> +</template> + +<script> +import Popover from 'src/components/popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faWrench } from '@fortawesome/free-solid-svg-icons' + +library.add( + faWrench +) + +export default { + components: { Popover }, + props: ['changed'] +} +</script> + +<style lang="scss"> +.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 index 86703697d260397379cb495b29cc127e34287959..2c833c0c080a5b0f48b986f3ddf9afcdcb019a20 100644 --- 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/settings_modal.js b/src/components/settings_modal/settings_modal.js index f0d49c91587b4ac32f960815c577663154621715..0404348334859659f66454cfc4fc6d702c572b6a 100644 --- a/src/components/settings_modal/settings_modal.js +++ b/src/components/settings_modal/settings_modal.js @@ -2,10 +2,55 @@ import Modal from 'src/components/modal/modal.vue' import PanelLoading from 'src/components/panel_loading/panel_loading.vue' import AsyncComponentError from 'src/components/async_component_error/async_component_error.vue' import getResettableAsyncComponent from 'src/services/resettable_async_component.js' +import Popover from '../popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { cloneDeep } from 'lodash' +import { + newImporter, + newExporter +} from 'src/services/export_import/export_import.js' +import { + faTimes, + faFileUpload, + faFileDownload, + faChevronDown +} from '@fortawesome/free-solid-svg-icons' +import { + faWindowMinimize +} from '@fortawesome/free-regular-svg-icons' + +const PLEROMAFE_SETTINGS_MAJOR_VERSION = 1 +const PLEROMAFE_SETTINGS_MINOR_VERSION = 0 + +library.add( + faTimes, + faWindowMinimize, + faFileUpload, + faFileDownload, + faChevronDown +) const SettingsModal = { + data () { + return { + dataImporter: newImporter({ + validator: this.importValidator, + onImport: this.onImport, + onImportFailure: this.onImportFailure + }), + dataThemeExporter: newExporter({ + filename: 'pleromafe_settings.full', + getExportedObject: () => this.generateExport(true) + }), + dataExporter: newExporter({ + filename: 'pleromafe_settings', + getExportedObject: () => this.generateExport() + }) + } + }, components: { Modal, + Popover, SettingsModalContent: getResettableAsyncComponent( () => import('./settings_modal_content.vue'), { @@ -21,6 +66,85 @@ const SettingsModal = { }, peekModal () { this.$store.dispatch('togglePeekSettingsModal') + }, + importValidator (data) { + if (!Array.isArray(data._pleroma_settings_version)) { + return { + messageKey: 'settings.file_import_export.invalid_file' + } + } + + const [major, minor] = data._pleroma_settings_version + + if (major > PLEROMAFE_SETTINGS_MAJOR_VERSION) { + return { + messageKey: 'settings.file_export_import.errors.file_too_new', + messageArgs: { + fileMajor: major, + feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION + } + } + } + + if (major < PLEROMAFE_SETTINGS_MAJOR_VERSION) { + return { + messageKey: 'settings.file_export_import.errors.file_too_old', + messageArgs: { + fileMajor: major, + feMajor: PLEROMAFE_SETTINGS_MAJOR_VERSION + } + } + } + + if (minor > PLEROMAFE_SETTINGS_MINOR_VERSION) { + this.$store.dispatch('pushGlobalNotice', { + level: 'warning', + messageKey: 'settings.file_export_import.errors.file_slightly_new' + }) + } + + return true + }, + onImportFailure (result) { + if (result.error) { + this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_settings_imported', level: 'error' }) + } else { + this.$store.dispatch('pushGlobalNotice', { ...result.validationResult, level: 'error' }) + } + }, + onImport (data) { + if (data) { this.$store.dispatch('loadSettings', data) } + }, + restore () { + this.dataImporter.importData() + }, + backup () { + this.dataExporter.exportData() + }, + backupWithTheme () { + this.dataThemeExporter.exportData() + }, + generateExport (theme = false) { + const { config } = this.$store.state + let sample = config + if (!theme) { + const ignoreList = new Set([ + 'customTheme', + 'customThemeSource', + 'colors' + ]) + sample = Object.fromEntries( + Object + .entries(sample) + .filter(([key]) => !ignoreList.has(key)) + ) + } + const clone = cloneDeep(sample) + clone._pleroma_settings_version = [ + PLEROMAFE_SETTINGS_MAJOR_VERSION, + PLEROMAFE_SETTINGS_MINOR_VERSION + ] + return clone } }, computed: { diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue index 552ca41f947a5b3ac2a2754f84815d598bbfc4c5..583c2ecc160f7772e29b512293c473bf3729fa39 100644 --- a/src/components/settings_modal/settings_modal.vue +++ b/src/components/settings_modal/settings_modal.vue @@ -31,20 +31,84 @@ </transition> <button class="btn button-default" + :title="$t('general.peek')" @click="peekModal" > - {{ $t('general.peek') }} + <FAIcon + :icon="['far', 'window-minimize']" + fixed-width + /> </button> <button class="btn button-default" + :title="$t('general.close')" @click="closeModal" > - {{ $t('general.close') }} + <FAIcon + icon="times" + fixed-width + /> </button> </div> <div class="panel-body"> <SettingsModalContent v-if="modalOpenedOnce" /> </div> + <div class="panel-footer"> + <Popover + class="export" + trigger="click" + placement="top" + :offset="{ y: 5, x: 5 }" + :bound-to="{ x: 'container' }" + remove-padding + > + <template v-slot:trigger> + <button + class="btn button-default" + :title="$t('general.close')" + > + <span>{{ $t("settings.file_export_import.backup_restore") }}</span> + <FAIcon + icon="chevron-down" + /> + </button> + </template> + <template v-slot:content="{close}"> + <div class="dropdown-menu"> + <button + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="backup" + @click="close" + > + <FAIcon + icon="file-download" + fixed-width + /><span>{{ $t("settings.file_export_import.backup_settings") }}</span> + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="backupWithTheme" + @click="close" + > + <FAIcon + icon="file-download" + fixed-width + /><span>{{ $t("settings.file_export_import.backup_settings_theme") }}</span> + </button> + <button + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="restore" + @click="close" + > + <FAIcon + icon="file-upload" + fixed-width + /><span>{{ $t("settings.file_export_import.restore_settings") }}</span> + </button> + </div> + </template> + </Popover> + </div> </div> </Modal> </template> diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_content.scss index f066234c846abd05d5dbafa397714bc3e58877c6..81ab434b6bee850ff6c7bef6b8e6cef8d82fdcfc 100644 --- a/src/components/settings_modal/settings_modal_content.scss +++ b/src/components/settings_modal/settings_modal_content.scss @@ -7,13 +7,24 @@ margin: 1em 1em 1.4em; padding-bottom: 1.4em; - > div { + > div, + > label { + display: block; margin-bottom: .5em; &:last-child { margin-bottom: 0; } } + .select-multiple { + display: flex; + + .option-list { + margin: 0; + padding-left: .5em; + } + } + &:last-child { border-bottom: none; padding-bottom: 0; diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index 5f38a5ae718c1a52dc2713060974e7dd145e0ad6..4eaf4217f202e8033dd7d1136b790844f51f6032 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -1,24 +1,23 @@ import { filter, trim } from 'lodash' -import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) const FilteringTab = { data () { return { - muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n') + muteWordsStringLocal: this.$store.getters.mergedConfig.muteWords.join('\n'), + replyVisibilityOptions: ['all', 'following', 'self'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.reply_visibility_${mode}`) + })) } }, components: { - Checkbox + BooleanSetting, + ChoiceSetting }, computed: { ...SharedComputedObject(), diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue index 8f850c8b7a6af1072fe3f408de34f1665505a100..50ee20e096f02eafe28951e450f11f849827c331 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -1,89 +1,138 @@ <template> <div :label="$t('settings.filtering')"> <div class="setting-item"> - <div class="select-multiple"> - <span class="label">{{ $t('settings.notification_visibility') }}</span> - <ul class="option-list"> - <li> - <Checkbox v-model="notificationVisibility.likes"> - {{ $t('settings.notification_visibility_likes') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.repeats"> - {{ $t('settings.notification_visibility_repeats') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.follows"> - {{ $t('settings.notification_visibility_follows') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.mentions"> - {{ $t('settings.notification_visibility_mentions') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.moves"> - {{ $t('settings.notification_visibility_moves') }} - </Checkbox> - </li> - <li> - <Checkbox v-model="notificationVisibility.emojiReactions"> - {{ $t('settings.notification_visibility_emoji_reactions') }} - </Checkbox> - </li> - </ul> - </div> - <div> - {{ $t('settings.replies_in_timeline') }} - <label - for="replyVisibility" - class="select" - > - <select - id="replyVisibility" - v-model="replyVisibility" + <h2>{{ $t('settings.posts') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting path="hideFilteredStatuses"> + {{ $t('settings.hide_filtered_statuses') }} + </BooleanSetting> + <ul + class="setting-list suboptions" + :class="[{disabled: !streaming}]" > - <option - value="all" - selected - >{{ $t('settings.reply_visibility_all') }}</option> - <option value="following">{{ $t('settings.reply_visibility_following') }}</option> - <option value="self">{{ $t('settings.reply_visibility_self') }}</option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" + <li> + <BooleanSetting + :disabled="hideFilteredStatuses" + path="hideWordFilteredPosts" + > + {{ $t('settings.hide_wordfiltered_statuses') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + :disabled="hideFilteredStatuses" + path="hideMutedThreads" + > + {{ $t('settings.hide_muted_threads') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting + :disabled="hideFilteredStatuses" + path="hideMutedPosts" + > + {{ $t('settings.hide_muted_posts') }} + </BooleanSetting> + </li> + </ul> + </li> + <li> + <BooleanSetting path="hidePostStats"> + {{ $t('settings.hide_post_stats') }} + </BooleanSetting> + </li> + <ChoiceSetting + id="replyVisibility" + path="replyVisibility" + :options="replyVisibilityOptions" + > + {{ $t('settings.replies_in_timeline') }} + </ChoiceSetting> + <li> + <h3>{{ $t('settings.wordfilter') }}</h3> + <textarea + id="muteWords" + v-model="muteWordsString" + class="resize-height" /> - </label> - </div> - <div> - <Checkbox v-model="hidePostStats"> - {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }} - </Checkbox> - </div> - <div> - <Checkbox v-model="hideUserStats"> - {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }} - </Checkbox> - </div> + <div>{{ $t('settings.filtering_explanation') }}</div> + </li> + <h3>{{ $t('settings.attachments') }}</h3> + <li> + <label for="maxThumbnails"> + {{ $t('settings.max_thumbnails') }} + </label> + <input + id="maxThumbnails" + path.number="maxThumbnails" + class="number-input" + type="number" + min="0" + step="1" + > + </li> + <li> + <BooleanSetting path="hideAttachments"> + {{ $t('settings.hide_attachments_in_tl') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="hideAttachmentsInConv"> + {{ $t('settings.hide_attachments_in_convo') }} + </BooleanSetting> + </li> + </ul> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.user_profiles') }}</h2> + <ul class="setting-list"> + <li> + <BooleanSetting path="hideUserStats"> + {{ $t('settings.hide_user_stats') }} + </BooleanSetting> + </li> + </ul> </div> <div class="setting-item"> - <div> - <p>{{ $t('settings.filtering_explanation') }}</p> - <textarea - id="muteWords" - class="resize-height" - v-model="muteWordsString" - /> - </div> - <div> - <Checkbox v-model="hideFilteredStatuses"> - {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }} - </Checkbox> - </div> + <h2>{{ $t('settings.notifications') }}</h2> + <ul class="setting-list"> + <li class="select-multiple"> + <span class="label">{{ $t('settings.notification_visibility') }}</span> + <ul class="option-list"> + <li> + <BooleanSetting path="notificationVisibility.likes"> + {{ $t('settings.notification_visibility_likes') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.repeats"> + {{ $t('settings.notification_visibility_repeats') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.follows"> + {{ $t('settings.notification_visibility_follows') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.mentions"> + {{ $t('settings.notification_visibility_mentions') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.moves"> + {{ $t('settings.notification_visibility_moves') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="notificationVisibility.emojiReactions"> + {{ $t('settings.notification_visibility_emoji_reactions') }} + </BooleanSetting> + </li> + </ul> + </li> + </ul> </div> </div> </template> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index 029ee7a1cc59076b2a78444dbbc7de3eb2cd1a56..952c328dee090491afd726e9cf263430f18928f3 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -1,21 +1,30 @@ -import Checkbox from 'src/components/checkbox/checkbox.vue' +import BooleanSetting from '../helpers/boolean_setting.vue' +import ChoiceSetting from '../helpers/choice_setting.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' import { library } from '@fortawesome/fontawesome-svg-core' import { - faChevronDown, faGlobe } from '@fortawesome/free-solid-svg-icons' library.add( - faChevronDown, faGlobe ) const GeneralTab = { data () { return { + subjectLineOptions: ['email', 'noop', 'masto'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.subject_line_${mode === 'masto' ? 'mastodon' : mode}`) + })), + mentionLinkDisplayOptions: ['short', 'full_for_remote', 'full'].map(mode => ({ + key: mode, + value: mode, + label: this.$t(`settings.mention_link_display_${mode}`) + })), loopSilentAvailable: // Firefox Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || @@ -26,18 +35,27 @@ const GeneralTab = { } }, components: { - Checkbox, + BooleanSetting, + ChoiceSetting, InterfaceLanguageSwitcher }, computed: { postFormats () { return this.$store.state.instance.postFormats || [] }, + postContentOptions () { + return this.postFormats.map(format => ({ + key: format, + value: format, + label: this.$t(`post_status.content_type["${format}"]`) + })) + }, instanceSpecificPanelPresent () { return this.$store.state.instance.showInstanceSpecificPanel }, instanceWallpaperUsed () { return this.$store.state.instance.background && !this.$store.state.users.currentUser.background_image }, + instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, ...SharedComputedObject() } } diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index a9081793bdd9361376f6968b91df0289723e9642..eba3b2680ddb4ce2b40ca196b835ba97aee8af10 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -7,228 +7,126 @@ <interface-language-switcher /> </li> <li v-if="instanceSpecificPanelPresent"> - <Checkbox v-model="hideISP"> + <BooleanSetting path="hideISP"> {{ $t('settings.hide_isp') }} - </Checkbox> + </BooleanSetting> + </li> + <li> + <BooleanSetting path="sidebarRight"> + {{ $t('settings.right_sidebar') }} + </BooleanSetting> </li> <li v-if="instanceWallpaperUsed"> - <Checkbox v-model="hideInstanceWallpaper"> + <BooleanSetting path="hideInstanceWallpaper"> {{ $t('settings.hide_wallpaper') }} - </Checkbox> + </BooleanSetting> </li> - </ul> - </div> - <div class="setting-item"> - <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> - </li> - <li> - <Checkbox v-model="collapseMessageWithSubject"> - {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="stopGifs"> + {{ $t('settings.stop_gifs') }} + </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> - </li> - <li> - <Checkbox v-model="emojiReactionsOnTimeline"> - {{ $t('settings.emoji_reactions_on_timeline') }} - </Checkbox> + </BooleanSetting> </li> <li> - <Checkbox v-model="virtualScrolling"> + <BooleanSetting path="virtualScrolling"> {{ $t('settings.virtual_scrolling') }} - </Checkbox> - </li> - </ul> - </div> - - <div class="setting-item"> - <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> - </li> - <li> - <Checkbox v-model="alwaysShowSubjectInput"> - {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }} - </Checkbox> + </BooleanSetting> </li> <li> - <div> - {{ $t('settings.subject_line_behavior') }} - <label - for="subjectLineBehavior" - class="select" - > - <select - id="subjectLineBehavior" - v-model="subjectLineBehavior" - > - <option value="email"> - {{ $t('settings.subject_line_email') }} - {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }} - </option> - <option value="masto"> - {{ $t('settings.subject_line_mastodon') }} - {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }} - </option> - <option value="noop"> - {{ $t('settings.subject_line_noop') }} - {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> - </div> - </li> - <li v-if="postFormats.length > 0"> - <div> - {{ $t('settings.post_status_content_type') }} - <label - for="postContentType" - class="select" - > - <select - id="postContentType" - v-model="postContentType" - > - <option - v-for="postFormat in postFormats" - :key="postFormat" - :value="postFormat" - > - {{ $t(`post_status.content_type["${postFormat}"]`) }} - {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> - </div> - </li> - <li> - <Checkbox v-model="minimalScopesMode"> - {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }} - </Checkbox> - </li> - <li> - <Checkbox v-model="autohideFloatingPostButton"> + <BooleanSetting path="autohideFloatingPostButton"> {{ $t('settings.autohide_floating_post_button') }} - </Checkbox> + </BooleanSetting> </li> - <li> - <Checkbox v-model="padEmoji"> - {{ $t('settings.pad_emoji') }} - </Checkbox> + <li v-if="instanceShoutboxPresent"> + <BooleanSetting path="hideShoutbox"> + {{ $t('settings.hide_shoutbox') }} + </BooleanSetting> </li> </ul> </div> - <div class="setting-item"> - <h2>{{ $t('settings.attachments') }}</h2> + <h2>{{ $t('settings.post_look_feel') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="hideAttachments"> - {{ $t('settings.hide_attachments_in_tl') }} - </Checkbox> + <BooleanSetting path="collapseMessageWithSubject"> + {{ $t('settings.collapse_subject') }} + </BooleanSetting> </li> <li> - <Checkbox v-model="hideAttachmentsInConv"> - {{ $t('settings.hide_attachments_in_convo') }} - </Checkbox> + <BooleanSetting path="emojiReactionsOnTimeline"> + {{ $t('settings.emoji_reactions_on_timeline') }} + </BooleanSetting> </li> + <h3>{{ $t('settings.attachments') }}</h3> <li> - <label for="maxThumbnails"> - {{ $t('settings.max_thumbnails') }} - </label> - <input - id="maxThumbnails" - v-model.number="maxThumbnails" - class="number-input" - type="number" - min="0" - step="1" - > + <BooleanSetting path="useContainFit"> + {{ $t('settings.use_contain_fit') }} + </BooleanSetting> </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"> - {{ $t('settings.stop_gifs') }} - </Checkbox> - </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,36 +137,130 @@ </ul> </li> <li> - <Checkbox v-model="playVideosInModal"> + <BooleanSetting path="playVideosInModal"> {{ $t('settings.play_videos_in_modal') }} - </Checkbox> + </BooleanSetting> </li> + <h3>{{ $t('settings.fun') }}</h3> <li> - <Checkbox v-model="useContainFit"> - {{ $t('settings.use_contain_fit') }} - </Checkbox> + <BooleanSetting path="greentext"> + {{ $t('settings.greentext') }} + </BooleanSetting> </li> + <li> + <BooleanSetting path="mentionLinkShowYous"> + {{ $t('settings.show_yous') }} + </BooleanSetting> + </li> + <li> + <ChoiceSetting + id="mentionLinkDisplay" + path="mentionLinkDisplay" + :options="mentionLinkDisplayOptions" + > + {{ $t('settings.mention_link_display') }} + </ChoiceSetting> + </li> + <ul + class="setting-list suboptions" + > + <li + v-if="mentionLinkDisplay === 'short'" + > + <BooleanSetting path="mentionLinkShowTooltip"> + {{ $t('settings.mention_link_show_tooltip') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="useAtIcon"> + {{ $t('settings.use_at_icon') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="mentionLinkShowAvatar"> + {{ $t('settings.mention_link_show_avatar') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="mentionLinkFadeDomain"> + {{ $t('settings.mention_link_fade_domain') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="mentionLinkBoldenYou"> + {{ $t('settings.mention_link_bolden_you') }} + </BooleanSetting> + </li> + </ul> </ul> </div> <div class="setting-item"> - <h2>{{ $t('settings.notifications') }}</h2> + <h2>{{ $t('settings.composing') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="webPushNotifications"> - {{ $t('settings.enable_web_push_notifications') }} - </Checkbox> + <BooleanSetting path="scopeCopy"> + {{ $t('settings.scope_copy') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="alwaysShowSubjectInput"> + {{ $t('settings.subject_input_always_show') }} + </BooleanSetting> + </li> + <li> + <ChoiceSetting + id="subjectLineBehavior" + path="subjectLineBehavior" + :options="subjectLineOptions" + > + {{ $t('settings.subject_line_behavior') }} + </ChoiceSetting> + </li> + <li v-if="postFormats.length > 0"> + <ChoiceSetting + id="postContentType" + path="postContentType" + :options="postContentOptions" + > + {{ $t('settings.post_status_content_type') }} + </ChoiceSetting> + </li> + <li> + <BooleanSetting path="minimalScopesMode"> + {{ $t('settings.minimal_scopes_mode') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="sensitiveByDefault"> + {{ $t('settings.sensitive_by_default') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="alwaysShowNewPostButton"> + {{ $t('settings.always_show_post_button') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="autohideFloatingPostButton"> + {{ $t('settings.autohide_floating_post_button') }} + </BooleanSetting> + </li> + <li> + <BooleanSetting path="padEmoji"> + {{ $t('settings.pad_emoji') }} + </BooleanSetting> </li> </ul> </div> <div class="setting-item"> - <h2>{{ $t('settings.fun') }}</h2> + <h2>{{ $t('settings.notifications') }}</h2> <ul class="setting-list"> <li> - <Checkbox v-model="greentext"> - {{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }} - </Checkbox> + <BooleanSetting path="webPushNotifications"> + {{ $t('settings.enable_web_push_notifications') }} + </BooleanSetting> </li> </ul> </div> diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue index 63d36bf9a2effe23d99932d6d91b941d5a6b4016..32a21415c6994454df69458061180a3e9a2b83b8 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue @@ -10,20 +10,18 @@ :query="queryUserIds" :placeholder="$t('settings.search_user_to_block')" > - <BlockCard - slot-scope="row" - :user-id="row.item" - /> + <template v-slot="row"> + <BlockCard + :user-id="row.item" + /> + </template> </Autosuggest> </div> <BlockList :refresh="true" :get-key="i => i" > - <template - slot="header" - slot-scope="{selected}" - > + <template v-slot:header="{selected}"> <div class="bulk-actions"> <ProgressButton v-if="selected.length > 0" @@ -31,7 +29,7 @@ :click="() => blockUsers(selected)" > {{ $t('user_card.block') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('user_card.block_progress') }} </template> </ProgressButton> @@ -41,19 +39,16 @@ :click="() => unblockUsers(selected)" > {{ $t('user_card.unblock') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('user_card.unblock_progress') }} </template> </ProgressButton> </div> </template> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <BlockCard :user-id="item" /> </template> - <template slot="empty"> + <template v-slot:empty> {{ $t('settings.no_blocks') }} </template> </BlockList> @@ -68,20 +63,18 @@ :query="queryUserIds" :placeholder="$t('settings.search_user_to_mute')" > - <MuteCard - slot-scope="row" - :user-id="row.item" - /> + <template v-slot="row"> + <MuteCard + :user-id="row.item" + /> + </template> </Autosuggest> </div> <MuteList :refresh="true" :get-key="i => i" > - <template - slot="header" - slot-scope="{selected}" - > + <template v-slot:header="{selected}"> <div class="bulk-actions"> <ProgressButton v-if="selected.length > 0" @@ -89,7 +82,7 @@ :click="() => muteUsers(selected)" > {{ $t('user_card.mute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('user_card.mute_progress') }} </template> </ProgressButton> @@ -99,19 +92,16 @@ :click="() => unmuteUsers(selected)" > {{ $t('user_card.unmute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('user_card.unmute_progress') }} </template> </ProgressButton> </div> </template> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <MuteCard :user-id="item" /> </template> - <template slot="empty"> + <template v-slot:empty> {{ $t('settings.no_mutes') }} </template> </MuteList> @@ -124,20 +114,18 @@ :query="queryKnownDomains" :placeholder="$t('settings.type_domains_to_mute')" > - <DomainMuteCard - slot-scope="row" - :domain="row.item" - /> + <template v-slot="row"> + <DomainMuteCard + :domain="row.item" + /> + </template> </Autosuggest> </div> <DomainMuteList :refresh="true" :get-key="i => i" > - <template - slot="header" - slot-scope="{selected}" - > + <template v-slot:header="{selected}"> <div class="bulk-actions"> <ProgressButton v-if="selected.length > 0" @@ -145,19 +133,16 @@ :click="() => unmuteDomains(selected)" > {{ $t('domain_mute_card.unmute') }} - <template slot="progress"> + <template v-slot:progress> {{ $t('domain_mute_card.unmute_progress') }} </template> </ProgressButton> </div> </template> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <DomainMuteCard :domain="item" /> </template> - <template slot="empty"> + <template v-slot:empty> {{ $t('settings.no_mutes') }} </template> </DomainMuteList> diff --git a/src/components/settings_modal/tabs/notifications_tab.vue b/src/components/settings_modal/tabs/notifications_tab.vue index 8f8fe48e1a98ce2fb139981c9711f289d566c337..7e0568ead0a51204a62bdfce3de54cff69ecbd5d 100644 --- a/src/components/settings_modal/tabs/notifications_tab.vue +++ b/src/components/settings_modal/tabs/notifications_tab.vue @@ -24,7 +24,7 @@ class="btn button-default" @click="updateNotificationSettings" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> </div> diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index 9709424c9e5a283c0876a0fbe8a8a26a16355f1b..64079fcdd6c212673b86ac83ff07f5d1e071b7c9 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -24,7 +24,7 @@ library.add( const ProfileTab = { data () { return { - newName: this.$store.state.users.currentUser.name, + newName: this.$store.state.users.currentUser.name_unescaped, newBio: unescape(this.$store.state.users.currentUser.description), newLocked: this.$store.state.users.currentUser.locked, newNoRichText: this.$store.state.users.currentUser.no_rich_text, diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index 175a0219b213946b59b9cc8809992d624a0142fd..bb3c301d5c175415044f3474e9ee4502ee374c18 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -153,7 +153,7 @@ class="btn button-default" @click="updateProfile" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> <div class="setting-item"> @@ -227,7 +227,7 @@ class="btn button-default" @click="submitBanner(banner)" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> <div class="setting-item"> @@ -266,7 +266,7 @@ class="btn button-default" @click="submitBackground(background)" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> </div> diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.js b/src/components/settings_modal/tabs/security_tab/security_tab.js index 811161a51b11a6afd6e0fb48fcdad07d8732ff1b..65d20fc0e39c06fcb80ea81971aed158c8285004 100644 --- 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/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue index 56bea1f466c9227d9da50f79e1a6e190182f39cd..275d46164b0e9e8ed88b46a5f6f2839b19799c47 100644 --- a/src/components/settings_modal/tabs/security_tab/security_tab.vue +++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -22,7 +22,7 @@ class="btn button-default" @click="changeEmail" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> <p v-if="changedEmail"> {{ $t('settings.changed_email') }} @@ -60,7 +60,7 @@ class="btn button-default" @click="changePassword" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> <p v-if="changedPassword"> {{ $t('settings.changed_password') }} @@ -133,7 +133,7 @@ class="btn button-default" @click="confirmDelete" > - {{ $t('general.submit') }} + {{ $t('settings.save') }} </button> </div> </div> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index 6cf75fe7102414f1067ca8f6a592e91a3fbf1c12..0b6669fcfaee50b54b3dbf21681d2c82a7463c62 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -15,6 +15,10 @@ import { shadows2to3, colors2to3 } from 'src/services/style_setter/style_setter.js' +import { + newImporter, + newExporter +} from 'src/services/export_import/export_import.js' import { SLOT_INHERITANCE } from 'src/services/theme_data/pleromafe.js' @@ -31,18 +35,10 @@ import ShadowControl from 'src/components/shadow_control/shadow_control.vue' import FontControl from 'src/components/font_control/font_control.vue' import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' -import ExportImport from 'src/components/export_import/export_import.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import Select from 'src/components/select/select.vue' import Preview from './preview.vue' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faChevronDown -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faChevronDown -) // List of color values used in v1 const v1OnlyNames = [ @@ -67,8 +63,18 @@ const colorConvert = (color) => { export default { data () { return { + themeImporter: newImporter({ + validator: this.importValidator, + onImport: this.onImport, + onImportFailure: this.onImportFailure + }), + themeExporter: newExporter({ + filename: 'pleroma_theme', + getExportedObject: () => this.exportedTheme + }), availableStyles: [], - selected: this.$store.getters.mergedConfig.theme, + selected: '', + selectedTheme: this.$store.getters.mergedConfig.theme, themeWarning: undefined, tempImportFile: undefined, engineVersion: 0, @@ -202,7 +208,7 @@ export default { } }, selectedVersion () { - return Array.isArray(this.selected) ? 1 : 2 + return Array.isArray(this.selectedTheme) ? 1 : 2 }, currentColors () { return Object.keys(SLOT_INHERITANCE) @@ -383,8 +389,8 @@ export default { FontControl, TabSwitcher, Preview, - ExportImport, - Checkbox + Checkbox, + Select }, methods: { loadTheme ( @@ -469,7 +475,7 @@ export default { this.loadThemeFromLocalStorage(false, true) break case 'file': - console.err('Forcing snapshout from file is not supported yet') + console.error('Forcing snapshot from file is not supported yet') break } this.dismissWarning() @@ -528,10 +534,15 @@ export default { this.previewColors.mod ) }, + importTheme () { this.themeImporter.importData() }, + exportTheme () { this.themeExporter.exportData() }, onImport (parsed, forceSource = false) { this.tempImportFile = parsed this.loadTheme(parsed, 'file', forceSource) }, + onImportFailure (result) { + this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' }) + }, importValidator (parsed) { const version = parsed._pleroma_theme_version return version >= 1 || version <= 2 @@ -735,6 +746,16 @@ export default { } }, selected () { + this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => { + if (Array.isArray(s)) { + console.log(s[0] === this.selected, this.selected) + return s[0] === this.selected + } else { + return s.name === this.selected + } + })[1] + }, + selectedTheme () { this.dismissWarning() if (this.selectedVersion === 1) { if (!this.keepRoundness) { @@ -752,17 +773,17 @@ export default { if (!this.keepColor) { this.clearV1() - this.bgColorLocal = this.selected[1] - this.fgColorLocal = this.selected[2] - this.textColorLocal = this.selected[3] - this.linkColorLocal = this.selected[4] - this.cRedColorLocal = this.selected[5] - this.cGreenColorLocal = this.selected[6] - this.cBlueColorLocal = this.selected[7] - this.cOrangeColorLocal = this.selected[8] + this.bgColorLocal = this.selectedTheme[1] + this.fgColorLocal = this.selectedTheme[2] + this.textColorLocal = this.selectedTheme[3] + this.linkColorLocal = this.selectedTheme[4] + this.cRedColorLocal = this.selectedTheme[5] + this.cGreenColorLocal = this.selectedTheme[6] + this.cBlueColorLocal = this.selectedTheme[7] + this.cOrangeColorLocal = this.selectedTheme[8] } } else if (this.selectedVersion >= 2) { - this.normalizeLocalState(this.selected.theme, 2, this.selected.source) + this.normalizeLocalState(this.selectedTheme.theme, 2, this.selectedTheme.source) } } } diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss index 1b7d9f31524367b814c3e2f069f4b35bbbe32224..0db21537fae4d5ee78e0132fd498578d9204c130 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.scss +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.scss @@ -270,6 +270,9 @@ .apply-container { justify-content: center; + position: absolute; + bottom: 8px; + right: 5px; } .radius-item, diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue index b8add42f48973d2dba28e6faf8e46461f39a5997..c02986edde0e58f31a0d8722c8954fc9393fd509 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -48,46 +48,47 @@ </template> </div> </div> - <ExportImport - :export-object="exportedTheme" - :export-label="$t("settings.export_theme")" - :import-label="$t("settings.import_theme")" - :import-failed-text="$t("settings.invalid_theme_imported")" - :on-import="onImport" - :validator="importValidator" - > - <template slot="before"> - <div class="presets"> - {{ $t('settings.presets') }} - <label - for="preset-switcher" - class="select" + <div class="top"> + <div class="presets"> + {{ $t('settings.presets') }} + <label + for="preset-switcher" + class="select" + > + <Select + id="preset-switcher" + v-model="selected" + class="preset-switcher" > - <select - id="preset-switcher" - v-model="selected" - class="preset-switcher" + <option + v-for="style in availableStyles" + :key="style.name" + :value="style.name || style[0]" + :style="{ + backgroundColor: style[1] || (style.theme || style.source).colors.bg, + color: style[3] || (style.theme || style.source).colors.text + }" > - <option - v-for="style in availableStyles" - :key="style.name" - :value="style" - :style="{ - backgroundColor: style[1] || (style.theme || style.source).colors.bg, - color: style[3] || (style.theme || style.source).colors.text - }" - > - {{ style[0] || style.name }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> - </div> - </template> - </ExportImport> + {{ style[0] || style.name }} + </option> + </Select> + </label> + </div> + <div class="export-import"> + <button + class="btn button-default" + @click="importTheme" + > + {{ $t("settings.import_theme") }} + </button> + <button + class="btn button-default" + @click="exportTheme" + > + {{ $t("settings.export_theme") }} + </button> + </div> + </div> </div> <div class="save-load-options"> <span class="keep-option"> @@ -902,28 +903,19 @@ <div class="tab-header shadow-selector"> <div class="select-container"> {{ $t('settings.style.shadows.component') }} - <label - for="shadow-switcher" - class="select" + <Select + id="shadow-switcher" + v-model="shadowSelected" + class="shadow-switcher" > - <select - id="shadow-switcher" - v-model="shadowSelected" - class="shadow-switcher" + <option + v-for="shadow in shadowsAvailable" + :key="shadow" + :value="shadow" > - <option - v-for="shadow in shadowsAvailable" - :key="shadow" - :value="shadow" - > - {{ $t('settings.style.shadows.components.' + shadow) }} - </option> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + {{ $t('settings.style.shadows.components.' + shadow) }} + </option> + </Select> </div> <div class="override"> <label diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js index 800c39d51b37588283ae4b1b2d4eb6f4b3587a68..2d5d6eb1650bda75ded55324e595d0e49baa7b31 100644 --- a/src/components/shadow_control/shadow_control.js +++ b/src/components/shadow_control/shadow_control.js @@ -1,5 +1,6 @@ import ColorInput from '../color_input/color_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' +import Select from '../select/select.vue' import { getCssShadow } from '../../services/style_setter/style_setter.js' import { hex2rgb } from '../../services/color_convert/color_convert.js' import { library } from '@fortawesome/fontawesome-svg-core' @@ -45,7 +46,8 @@ export default { }, components: { ColorInput, - OpacityInput + OpacityInput, + Select }, methods: { add () { diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index 37d491f046a4d493a58b59e47c1e53aa8ade8f99..511e07f3f1dbfd480eff93cd84c196abe9ee1cd2 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -59,30 +59,20 @@ :disabled="usingFallback" class="id-control style-control" > - <label - for="shadow-switcher" - class="select" + <Select + id="shadow-switcher" + v-model="selectedId" + class="shadow-switcher" :disabled="!ready || usingFallback" > - <select - id="shadow-switcher" - v-model="selectedId" - class="shadow-switcher" - :disabled="!ready || usingFallback" + <option + v-for="(shadow, index) in cValue" + :key="index" + :value="index" > - <option - v-for="(shadow, index) in cValue" - :key="index" - :value="index" - > - {{ $t('settings.style.shadows.shadow_id', { value: index }) }} - </option> - </select> - <FAIcon - icon="chevron-down" - class="select-down-icon" - /> - </label> + {{ $t('settings.style.shadows.shadow_id', { value: index }) }} + </option> + </Select> <button class="btn button-default" :disabled="!ready || !present" @@ -316,20 +306,20 @@ .id-control { align-items: stretch; - .select, .btn { + + .shadow-switcher { + flex: 1; + } + + .shadow-switcher, .btn { min-width: 1px; margin-right: 5px; } + .btn { padding: 0 .4em; margin: 0 .1em; } - .select { - flex: 1; - select { - align-self: initial; - } - } } } } diff --git a/src/components/chat_panel/chat_panel.js b/src/components/shout_panel/shout_panel.js similarity index 57% rename from src/components/chat_panel/chat_panel.js rename to src/components/shout_panel/shout_panel.js index c3887098025fe2228f45c543cb9660fa41041274..a6168971813bd6fea6a7f35f11b5bf4b62fbe998 100644 --- a/src/components/chat_panel/chat_panel.js +++ b/src/components/shout_panel/shout_panel.js @@ -10,7 +10,7 @@ library.add( faTimes ) -const chatPanel = { +const shoutPanel = { props: [ 'floating' ], data () { return { @@ -21,12 +21,12 @@ const chatPanel = { }, computed: { messages () { - return this.$store.state.chat.messages + return this.$store.state.shout.messages } }, methods: { submit (message) { - this.$store.state.chat.channel.push('new_msg', { text: message }, 10000) + this.$store.state.shout.channel.push('new_msg', { text: message }, 10000) this.currentMessage = '' }, togglePanel () { @@ -35,7 +35,19 @@ 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 + }) + } + } } } -export default chatPanel +export default shoutPanel diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/shout_panel/shout_panel.vue similarity index 69% rename from src/components/chat_panel/chat_panel.vue rename to src/components/shout_panel/shout_panel.vue index 7993c94d9a7e9cdcf33f350de86175adf632bf0b..c88797d130a164b27f3950ddca02f870e13ad2b3 100644 --- a/src/components/chat_panel/chat_panel.vue +++ b/src/components/shout_panel/shout_panel.vue @@ -1,52 +1,50 @@ <template> <div v-if="!collapsed || !floating" - class="chat-panel" + class="shout-panel" > <div class="panel panel-default"> <div class="panel-heading timeline-heading" - :class="{ 'chat-heading': floating }" + :class="{ 'shout-heading': floating }" @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="shout-window"> <div v-for="message in messages" :key="message.id" - class="chat-message" + class="shout-message" > - <span class="chat-avatar"> + <span class="shout-avatar"> <img :src="message.author.avatar"> </span> - <div class="chat-content"> + <div class="shout-content"> <router-link - class="chat-name" + class="shout-name" :to="userProfileLink(message.author)" > {{ message.author.username }} </router-link> <br> - <span class="chat-text"> + <span class="shout-text"> {{ message.text }} </span> </div> </div> </div> - <div class="chat-input"> + <div class="shout-input"> <textarea v-model="currentMessage" - class="chat-input-textarea" + class="shout-input-textarea" rows="1" @keyup.enter="submit(currentMessage)" /> @@ -55,11 +53,11 @@ </div> <div v-else - class="chat-panel" + class="shout-panel" > <div class="panel panel-default"> <div - class="panel-heading stub timeline-heading chat-heading" + class="panel-heading stub timeline-heading shout-heading" @click.stop.prevent="togglePanel" > <div class="title"> @@ -74,45 +72,59 @@ </div> </template> -<script src="./chat_panel.js"></script> +<script src="./shout_panel.js"></script> <style lang="scss"> @import '../../_variables.scss'; -.floating-chat { +.floating-shout { position: fixed; - right: 0px; bottom: 0px; z-index: 1000; max-width: 25em; } -.chat-panel { - .chat-heading { +.floating-shout.left { + left: 0px; +} + +.floating-shout:not(.left) { + right: 0px; +} + +.shout-panel { + .shout-heading { cursor: pointer; .icon { color: $fallback--text; color: var(--text, $fallback--text); + margin-right: 0.5em; + } + + .title { + display: flex; + justify-content: space-between; + align-items: center; } } - .chat-window { + .shout-window { overflow-y: auto; overflow-x: hidden; max-height: 20em; } - .chat-window-container { + .shout-window-container { height: 100%; } - .chat-message { + .shout-message { display: flex; padding: 0.2em 0.5em } - .chat-avatar { + .shout-avatar { img { height: 24px; width: 24px; @@ -123,7 +135,7 @@ } } - .chat-input { + .shout-input { display: flex; textarea { flex: 1; @@ -133,7 +145,7 @@ } } - .chat-panel { + .shout-panel { .title { display: flex; justify-content: space-between; diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index fe73616808744b5bc889770f362f0f6bc324d6fb..89719df36d35eb442ea0819a4b5d13226b8fac7a 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -49,7 +49,7 @@ const SideDrawer = { currentUser () { return this.$store.state.users.currentUser }, - chat () { return this.$store.state.chat.channel.state === 'joined' }, + shout () { return this.$store.state.shout.channel.state === 'joined' }, unseenNotifications () { return unseenNotificationsFromStore(this.$store) }, diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 223b16321496cf4917f7f07e360c6383a83bd4d5..dd88de7d5d9ec52df1f5135a2703e4269daf267d 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -106,10 +106,10 @@ </router-link> </li> <li - v-if="chat" + v-if="shout" @click="toggleDrawer" > - <router-link :to="{ name: 'chat-panel' }"> + <router-link :to="{ name: 'shout-panel' }"> <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -273,9 +273,7 @@ --icon: var(--popoverIcon, $fallback--icon); .badge { - position: absolute; - right: 0.7rem; - top: 1em; + margin-left: 10px; } } diff --git a/src/components/staff_panel/staff_panel.js b/src/components/staff_panel/staff_panel.js index 8665648a5dd57f75d1fe25ce3d9b84f6fee0e79d..b9561bf191c1f93ad68f27a6130e20b6793ab0f3 100644 --- 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 index 1d13003df10e40466ff4e6085448b1275e13cd87..c52ade4259897ebaf010f6e5fd2562399c2cb0b0 100644 --- 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 index 2bf93a9eb62e54f30fe6589389ff596e87322b35..d8f94926e5f58fa893a9a6e4d238f37e96cc8a69 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -9,9 +9,12 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import AvatarList from '../avatar_list/avatar_list.vue' import Timeago from '../timeago/timeago.vue' import StatusContent from '../status_content/status_content.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import StatusPopover from '../status_popover/status_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' +import MentionsLine from 'src/components/mentions_line/mentions_line.vue' +import MentionLink from 'src/components/mention_link/mention_link.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { muteWordHits } from '../../services/status_parser/status_parser.js' @@ -68,7 +71,10 @@ const Status = { StatusPopover, UserListPopover, EmojiReactions, - StatusContent + StatusContent, + RichContent, + MentionLink, + MentionsLine }, props: [ 'statusoid', @@ -92,7 +98,8 @@ const Status = { userExpanded: false, mediaPlaying: [], suspendable: true, - error: null + error: null, + headTailLinks: null } }, computed: { @@ -132,12 +139,15 @@ const Status = { }, replyProfileLink () { if (this.isReply) { - return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName) + const user = this.$store.getters.findUser(this.status.in_reply_to_user_id) + // FIXME Why user not found sometimes??? + return user ? user.statusnet_profile_url : 'NOT_FOUND' } }, retweet () { return !!this.statusoid.retweeted_status }, - retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name }, - retweeterHtml () { return this.statusoid.user.name_html }, + retweeterUser () { return this.statusoid.user }, + retweeter () { return this.statusoid.user.name || this.statusoid.user.screen_name_ui }, + retweeterHtml () { return this.statusoid.user.name }, retweeterProfileLink () { return this.generateUserProfileLink(this.statusoid.user.id, this.statusoid.user.screen_name) }, status () { if (this.retweet) { @@ -156,27 +166,52 @@ const Status = { muteWordHits () { return muteWordHits(this.status, this.muteWords) }, + mentionsLine () { + if (!this.headTailLinks) return [] + const writtenSet = new Set(this.headTailLinks.writtenMentions.map(_ => _.url)) + return this.status.attentions.filter(attn => { + // no reply user + return attn.id !== this.status.in_reply_to_user_id && + // no self-replies + attn.statusnet_profile_url !== this.status.user.statusnet_profile_url && + // don't include if mentions is written + !writtenSet.has(attn.statusnet_profile_url) + }).map(attn => ({ + url: attn.statusnet_profile_url, + content: attn.screen_name, + userId: attn.id + })) + }, + hasMentionsLine () { + return this.mentionsLine.length > 0 + }, muted () { + if (this.statusoid.user.id === this.currentUser.id) return false + const reasonsToMute = this.userIsMuted || + // Thread is muted + status.thread_muted || + // Wordfiltered + this.muteWordHits.length > 0 + return !this.unmuted && !this.shouldNotMute && reasonsToMute + }, + userIsMuted () { 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) const relationshipReblog = reblog && this.$store.getters.relationship(reblog.user.id) - const reasonsToMute = ( - // Post is muted according to BE - status.muted || + return status.muted || // Reprööt of a muted post according to BE (reblog && reblog.muted) || // Muted user relationship.muting || // Muted user of a reprööt - (relationshipReblog && relationshipReblog.muting) || - // Thread is muted - status.thread_muted || - // Wordfiltered - this.muteWordHits.length > 0 - ) - const excusesNotToMute = ( + (relationshipReblog && relationshipReblog.muting) + }, + shouldNotMute () { + const { status } = this + const { reblog } = status + return ( ( this.inProfile && ( // Don't mute user's posts on user timeline (except reblogs) @@ -189,14 +224,26 @@ const Status = { (this.inConversation && status.thread_muted) // No excuses if post has muted words ) && !this.muteWordHits.length > 0 - - return !this.unmuted && !excusesNotToMute && reasonsToMute + }, + hideMutedUsers () { + return this.mergedConfig.hideMutedPosts + }, + hideMutedThreads () { + return this.mergedConfig.hideMutedThreads }, hideFilteredStatuses () { return this.mergedConfig.hideFilteredStatuses }, + hideWordFilteredPosts () { + return this.mergedConfig.hideWordFilteredPosts + }, hideStatus () { - return (this.muted && this.hideFilteredStatuses) || this.virtualHidden + return (this.virtualHidden || !this.shouldNotMute) && ( + (this.muted && this.hideFilteredStatuses) || + (this.userIsMuted && this.hideMutedUsers) || + (this.status.thread_muted && this.hideMutedThreads) || + (this.muteWordHits.length > 0 && this.hideWordFilteredPosts) + ) }, isFocused () { // retweet or root of an expanded conversation @@ -216,7 +263,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 () { @@ -303,6 +350,9 @@ const Status = { }, removeMediaPlaying (id) { this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id) + }, + setHeadTailLinks (headTailLinks) { + this.headTailLinks = headTailLinks } }, watch: { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 58b55bc81b1077311d3abefc786fd1b5963fb1b9..2028ade9e0ef15efcc62cb8fc5d910f73df16b35 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -1,10 +1,12 @@ - @import '../../_variables.scss'; $status-margin: 0.75em; .Status { min-width: 0; + white-space: normal; + word-wrap: break-word; + word-break: break-word; &:hover { --_still-image-img-visibility: visible; @@ -93,12 +95,8 @@ $status-margin: 0.75em; margin-right: 0.4em; text-overflow: ellipsis; - .emoji { - width: 14px; - height: 14px; - vertical-align: middle; - object-fit: contain; - } + --_still_image-label-scale: 0.25; + --emoji-size: 14px; } .status-favicon { @@ -155,42 +153,37 @@ $status-margin: 0.75em; } } + .glued-label { + display: inline-flex; + white-space: nowrap; + } + .timeago { margin-right: 0.2em; } - .heading-reply-row { + & .heading-reply-row { position: relative; align-content: baseline; font-size: 12px; - line-height: 18px; + margin-top: 0.2em; + line-height: 130%; max-width: 100%; - display: flex; - flex-wrap: wrap; align-items: stretch; } - .reply-to-and-accountname { - display: flex; - height: 18px; - margin-right: 0.5em; - max-width: 100%; - - .reply-to-link { - white-space: nowrap; - word-break: break-word; - text-overflow: ellipsis; - overflow-x: hidden; - } - } - & .reply-to-popover, - & .reply-to-no-popover { + & .reply-to-no-popover, + & .mentions { min-width: 0; margin-right: 0.4em; flex-shrink: 0; } + .reply-glued-label { + margin-right: 0.5em; + } + .reply-to-popover { .reply-to:hover::before { content: ''; @@ -220,21 +213,26 @@ $status-margin: 0.75em; } } - .reply-to { + & .mentions, + & .reply-to { + white-space: nowrap; position: relative; } - .reply-to-text { + & .mentions-text, + & .reply-to-text { + color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } - .replies-separator { - margin-left: 0.4em; + .mentions-line { + display: inline; } .replies { + margin-top: 0.25em; line-height: 18px; font-size: 12px; display: flex; diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 6ee8117f2a23b633e16291888f274f8e0dc7eff6..3bb29db66565bf898c9e1d75e19064d6651b093e 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -1,5 +1,4 @@ <template> - <!-- eslint-disable vue/no-v-html --> <div v-if="!hideStatus" class="Status" @@ -26,7 +25,7 @@ icon="retweet" /> <router-link :to="userProfileLink"> - {{ status.user.screen_name }} + {{ status.user.screen_name_ui }} </router-link> </small> <small @@ -89,8 +88,12 @@ <router-link v-if="retweeterHtml" :to="retweeterProfileLink" - v-html="retweeterHtml" - /> + > + <RichContent + :html="retweeterHtml" + :emoji="retweeterUser.emoji" + /> + </router-link> <router-link v-else :to="retweeterProfileLink" @@ -145,8 +148,12 @@ v-if="status.user.name_html" class="status-username" :title="status.user.name" - v-html="status.user.name_html" - /> + > + <RichContent + :html="status.user.name" + :emoji="status.user.emoji" + /> + </h4> <h4 v-else class="status-username" @@ -156,10 +163,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)" @@ -214,11 +221,13 @@ </button> </span> </div> - - <div class="heading-reply-row"> - <div + <div + v-if="isReply || hasMentionsLine" + class="heading-reply-row" + > + <span v-if="isReply" - class="reply-to-and-accountname" + class="glued-label reply-glued-label" > <StatusPopover v-if="!isPreview" @@ -238,7 +247,7 @@ flip="horizontal" /> <span - class="faint-link reply-to-text" + class="reply-to-text" > {{ $t('status.reply_to') }} </span> @@ -251,50 +260,76 @@ > <span class="reply-to-text">{{ $t('status.reply_to') }}</span> </span> - <router-link - class="reply-to-link" - :title="replyToName" - :to="replyProfileLink" - > - {{ replyToName }} - </router-link> - <span - v-if="replies && replies.length" - class="faint replies-separator" - > - - - </span> - </div> - <div - v-if="inConversation && !isPreview && replies && replies.length" - class="replies" + <MentionLink + :content="replyToName" + :url="replyProfileLink" + :user-id="status.in_reply_to_user_id" + :user-screen-name="status.in_reply_to_screen_name" + :first-mention="false" + /> + </span> + + <!-- This little wrapper is made for sole purpose of "gluing" --> + <!-- "Mentions" label to the first mention --> + <span + v-if="hasMentionsLine" + class="glued-label" > - <span class="faint">{{ $t('status.replies_list') }}</span> - <StatusPopover - v-for="reply in replies" - :key="reply.id" - :status-id="reply.id" + <span + class="mentions" + :aria-label="$t('tool_tip.mentions')" + @click.prevent="gotoOriginal(status.in_reply_to_status_id)" > - <button - class="button-unstyled -link reply-link" - @click.prevent="gotoOriginal(reply.id)" + <span + class="mentions-text" > - {{ reply.name }} - </button> - </StatusPopover> - </div> + {{ $t('status.mentions') }} + </span> + </span> + <MentionsLine + v-if="hasMentionsLine" + :mentions="mentionsLine.slice(0, 1)" + class="mentions-line-first" + /> + </span> + <MentionsLine + v-if="hasMentionsLine" + :mentions="mentionsLine.slice(1)" + class="mentions-line" + /> </div> </div> <StatusContent + ref="content" :status="status" :no-heading="noHeading" :highlight="highlight" :focused="isFocused" @mediaplay="addMediaPlaying($event)" @mediapause="removeMediaPlaying($event)" + @parseReady="setHeadTailLinks" /> + <div + v-if="inConversation && !isPreview && replies && replies.length" + class="replies" + > + <span class="faint">{{ $t('status.replies_list') }}</span> + <StatusPopover + v-for="reply in replies" + :key="reply.id" + :status-id="reply.id" + > + <button + class="button-unstyled -link reply-link" + @click.prevent="gotoOriginal(reply.id)" + > + {{ reply.name }} + </button> + </StatusPopover> + </div> + <transition name="fade"> <div v-if="!hidePostStats && isFocused && combinedFavsAndRepeatsUsers.length > 0" @@ -402,7 +437,6 @@ </div> </template> </div> -<!-- eslint-enable vue/no-v-html --> </template> <script src="./status.js" ></script> diff --git a/src/components/status_body/status_body.js b/src/components/status_body/status_body.js new file mode 100644 index 0000000000000000000000000000000000000000..91c331359dada3af0f2f201b661f2dee09afda8a --- /dev/null +++ b/src/components/status_body/status_body.js @@ -0,0 +1,129 @@ +import fileType from 'src/services/file_type/file_type.service' +import RichContent from 'src/components/rich_content/rich_content.jsx' +import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faFile, + faMusic, + faImage, + faLink, + faPollH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faFile, + faMusic, + faImage, + faLink, + faPollH +) + +const StatusContent = { + name: 'StatusContent', + props: [ + 'compact', + 'status', + 'focused', + 'noHeading', + 'fullContent', + 'singleLine' + ], + data () { + return { + showingTall: this.fullContent || (this.inConversation && this.focused), + showingLongSubject: false, + // not as computed because it sets the initial state which will be changed later + expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject, + postLength: this.status.text.length, + parseReadyDone: false + } + }, + computed: { + localCollapseSubjectDefault () { + return this.mergedConfig.collapseMessageWithSubject + }, + // This is a bit hacky, but we want to approximate post height before rendering + // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them) + // as well as approximate line count by counting characters and approximating ~80 + // per line. + // + // Using max-height + overflow: auto for status components resulted in false positives + // very often with japanese characters, and it was very annoying. + tallStatus () { + if (this.singleLine || this.compact) return false + const lengthScore = this.status.raw_html.split(/<p|<br/).length + this.postLength / 80 + return lengthScore > 20 + }, + longSubject () { + return this.status.summary.length > 240 + }, + // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status. + mightHideBecauseSubject () { + return !!this.status.summary && this.localCollapseSubjectDefault + }, + mightHideBecauseTall () { + return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault) + }, + hideSubjectStatus () { + return this.mightHideBecauseSubject && !this.expandingSubject + }, + hideTallStatus () { + return this.mightHideBecauseTall && !this.showingTall + }, + showingMore () { + return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject) + }, + attachmentTypes () { + return this.status.attachments.map(file => fileType.fileType(file.mimetype)) + }, + ...mapGetters(['mergedConfig']) + }, + components: { + RichContent + }, + mounted () { + this.status.attentions && this.status.attentions.forEach(attn => { + const { id } = attn + this.$store.dispatch('fetchUserIfMissing', id) + }) + }, + methods: { + onParseReady (event) { + if (this.parseReadyDone) return + this.parseReadyDone = true + this.$emit('parseReady', event) + const { writtenMentions, invisibleMentions } = event + writtenMentions + .filter(mention => !mention.notifying) + .forEach(mention => { + const { content, url } = mention + const cleanedString = content.replace(/<[^>]+?>/gi, '') // remove all tags + if (!cleanedString.startsWith('@')) return + const handle = cleanedString.slice(1) + const host = url.replace(/^https?:\/\//, '').replace(/\/.+?$/, '') + this.$store.dispatch('fetchUserIfMissing', `${handle}@${host}`) + }) + /* This is a bit of a hack to make current tall status detector work + * with rich mentions. Invisible mentions are detected at RichContent level + * and also we generate plaintext version of mentions by stripping tags + * so here we subtract from post length by each mention that became invisible + * via MentionsLine + */ + this.postLength = invisibleMentions.reduce((acc, mention) => { + return acc - mention.textContent.length - 1 + }, this.postLength) + }, + toggleShowMore () { + if (this.mightHideBecauseTall) { + this.showingTall = !this.showingTall + } else if (this.mightHideBecauseSubject) { + this.expandingSubject = !this.expandingSubject + } + }, + generateTagLink (tag) { + return `/tag/${tag}` + } + } +} + +export default StatusContent diff --git a/src/components/status_body/status_body.scss b/src/components/status_body/status_body.scss new file mode 100644 index 0000000000000000000000000000000000000000..f261108ebb7e50ce4ccdb9dfc36cdf5705808762 --- /dev/null +++ b/src/components/status_body/status_body.scss @@ -0,0 +1,174 @@ +@import '../../_variables.scss'; + +.StatusBody { + display: flex; + flex-direction: column; + + .emoji { + --_still_image-label-scale: 0.5; + } + + .attachments { + margin-top: 0.5em; + } + + & .text, + & .summary { + font-family: var(--postFont, sans-serif); + white-space: pre-wrap; + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + line-height: 1.4em; + } + + .summary { + display: block; + font-style: italic; + padding-bottom: 0.5em; + } + + .text { + &.-single-line { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + height: 1.4em; + } + } + + .summary-wrapper { + margin-bottom: 0.5em; + border-style: solid; + border-width: 0 0 1px 0; + border-color: var(--border, $fallback--border); + flex-grow: 0; + + &.-tall { + position: relative; + + .summary { + max-height: 2em; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } + + .text-wrapper { + display: flex; + flex-direction: column; + flex-wrap: nowrap; + + &.-tall-status { + position: relative; + height: 220px; + overflow-x: hidden; + overflow-y: hidden; + z-index: 1; + + .media-body { + min-height: 0; + mask: + linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, + linear-gradient(to top, white, white); + + /* Autoprefixed seem to ignore this one, and also syntax is different */ + -webkit-mask-composite: xor; + mask-composite: exclude; + } + } + } + + & .tall-status-hider, + & .tall-subject-hider, + & .status-unhider, + & .cw-status-hider { + display: inline-block; + word-break: break-all; + width: 100%; + text-align: center; + } + + .tall-status-hider { + position: absolute; + height: 70px; + margin-top: 150px; + line-height: 110px; + z-index: 2; + } + + .tall-subject-hider { + // position: absolute; + padding-bottom: 0.5em; + } + + & .status-unhider, + & .cw-status-hider { + word-break: break-all; + + svg { + color: inherit; + } + } + + .greentext { + color: $fallback--cGreen; + color: var(--postGreentext, $fallback--cGreen); + } + + .cyantext { + color: var(--postCyantext, $fallback--cBlue); + } + + &.-compact { + align-items: top; + flex-direction: row; + + --emoji-size: 16px; + + & .body, + & .attachments { + max-height: 3.25em; + } + + .body { + overflow: hidden; + white-space: normal; + min-width: 5em; + flex: 5 1 auto; + mask-size: auto 3.5em, auto auto; + mask-position: 0 0, 0 0; + mask-repeat: repeat-x, repeat; + mask-image: linear-gradient(to bottom, white 2em, transparent 3em); + + /* Autoprefixed seem to ignore this one, and also syntax is different */ + -webkit-mask-composite: xor; + mask-composite: exclude; + } + + .attachments { + margin-top: 0; + flex: 1 1 0; + min-width: 5em; + height: 100%; + margin-left: 0.5em; + } + + .summary-wrapper { + .summary::after { + content: ': '; + } + + line-height: inherit; + margin: 0; + border: none; + display: inline-block; + } + + .text-wrapper { + display: inline-block; + } + } +} diff --git a/src/components/status_body/status_body.vue b/src/components/status_body/status_body.vue new file mode 100644 index 0000000000000000000000000000000000000000..a088e6bc85692a359a82c4fdab774e1dadddb6c5 --- /dev/null +++ b/src/components/status_body/status_body.vue @@ -0,0 +1,100 @@ +<template> + <div + class="StatusBody" + :class="{ '-compact': compact }" + > + <div class="body"> + <div + v-if="status.summary_raw_html" + class="summary-wrapper" + :class="{ '-tall': (longSubject && !showingLongSubject) }" + > + <RichContent + class="media-body summary" + :html="status.summary_raw_html" + :emoji="status.emojis" + /> + <button + v-if="longSubject && showingLongSubject" + class="button-unstyled -link tall-subject-hider" + @click.prevent="showingLongSubject=false" + > + {{ $t("status.hide_full_subject") }} + </button> + <button + v-else-if="longSubject" + class="button-unstyled -link tall-subject-hider" + @click.prevent="showingLongSubject=true" + > + {{ $t("status.show_full_subject") }} + </button> + </div> + <div + :class="{'-tall-status': hideTallStatus}" + class="text-wrapper" + > + <button + v-if="hideTallStatus" + class="button-unstyled -link tall-status-hider" + :class="{ '-focused': focused }" + @click.prevent="toggleShowMore" + > + {{ $t("general.show_more") }} + </button> + <RichContent + v-if="!hideSubjectStatus && !(singleLine && status.summary_raw_html)" + :class="{ '-single-line': singleLine }" + class="text media-body" + :html="status.raw_html" + :emoji="status.emojis" + :handle-links="true" + :greentext="mergedConfig.greentext" + :attentions="status.attentions" + @parseReady="onParseReady" + /> + + <button + v-if="hideSubjectStatus" + class="button-unstyled -link cw-status-hider" + @click.prevent="toggleShowMore" + > + {{ $t("status.show_content") }} + <FAIcon + v-if="attachmentTypes.includes('image')" + icon="image" + /> + <FAIcon + v-if="attachmentTypes.includes('video')" + icon="video" + /> + <FAIcon + v-if="attachmentTypes.includes('audio')" + icon="music" + /> + <FAIcon + v-if="attachmentTypes.includes('unknown')" + icon="file" + /> + <FAIcon + v-if="status.poll && status.poll.options" + icon="poll-h" + /> + <FAIcon + v-if="status.card" + icon="link" + /> + </button> + <button + v-if="showingMore && !fullContent" + class="button-unstyled -link status-unhider" + @click.prevent="toggleShowMore" + > + {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }} + </button> + </div> + </div> + <slot v-if="!hideSubjectStatus" /> + </div> +</template> +<script src="./status_body.js" ></script> +<style lang="scss" src="./status_body.scss" /> diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index a6f79d766f0c4d87c6aeba00a41251ad398d107e..dec8914a32414cae8b1d9eb0868023229f5d8d26 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -1,11 +1,8 @@ import Attachment from '../attachment/attachment.vue' import Poll from '../poll/poll.vue' import Gallery from '../gallery/gallery.vue' +import StatusBody from 'src/components/status_body/status_body.vue' import LinkPreview from '../link-preview/link-preview.vue' -import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' -import fileType from 'src/services/file_type/file_type.service' -import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js' -import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' import { mapGetters, mapState } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -30,57 +27,17 @@ const StatusContent = { name: 'StatusContent', props: [ 'status', + 'compact', 'focused', 'noHeading', 'fullContent', 'singleLine' ], - data () { - return { - showingTall: this.fullContent || (this.inConversation && this.focused), - showingLongSubject: false, - // not as computed because it sets the initial state which will be changed later - expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject - } - }, computed: { - localCollapseSubjectDefault () { - return this.mergedConfig.collapseMessageWithSubject - }, hideAttachments () { return (this.mergedConfig.hideAttachments && !this.inConversation) || (this.mergedConfig.hideAttachmentsInConv && this.inConversation) }, - // This is a bit hacky, but we want to approximate post height before rendering - // so we count newlines (masto uses <p> for paragraphs, GS uses <br> between them) - // as well as approximate line count by counting characters and approximating ~80 - // per line. - // - // Using max-height + overflow: auto for status components resulted in false positives - // very often with japanese characters, and it was very annoying. - tallStatus () { - const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80 - return lengthScore > 20 - }, - longSubject () { - return this.status.summary.length > 240 - }, - // When a status has a subject and is also tall, we should only have one show more/less button. If the default is to collapse statuses with subjects, we just treat it like a status with a subject; otherwise, we just treat it like a tall status. - mightHideBecauseSubject () { - return !!this.status.summary && this.localCollapseSubjectDefault - }, - mightHideBecauseTall () { - return this.tallStatus && !(this.status.summary && this.localCollapseSubjectDefault) - }, - hideSubjectStatus () { - return this.mightHideBecauseSubject && !this.expandingSubject - }, - hideTallStatus () { - return this.mightHideBecauseTall && !this.showingTall - }, - showingMore () { - return (this.mightHideBecauseTall && this.showingTall) || (this.mightHideBecauseSubject && this.expandingSubject) - }, nsfwClickthrough () { if (!this.status.nsfw) { return false @@ -91,72 +48,20 @@ const StatusContent = { return true }, attachmentSize () { - if ((this.mergedConfig.hideAttachments && !this.inConversation) || + if (this.compact) { + return 'small' + } else if ((this.mergedConfig.hideAttachments && !this.inConversation) || (this.mergedConfig.hideAttachmentsInConv && this.inConversation) || (this.status.attachments.length > this.maxThumbnails)) { return 'hide' - } else if (this.compact) { - return 'small' } return 'normal' }, - galleryTypes () { - if (this.attachmentSize === 'hide') { - return [] - } - return this.mergedConfig.playVideosInModal - ? ['image', 'video'] - : ['image'] - }, - galleryAttachments () { - return this.status.attachments.filter( - file => fileType.fileMatchesSomeType(this.galleryTypes, file) - ) - }, - nonGalleryAttachments () { - return this.status.attachments.filter( - file => !fileType.fileMatchesSomeType(this.galleryTypes, file) - ) - }, - attachmentTypes () { - return this.status.attachments.map(file => fileType.fileType(file.mimetype)) - }, maxThumbnails () { return this.mergedConfig.maxThumbnails }, - postBodyHtml () { - const html = this.status.statusnet_html - - if (this.mergedConfig.greentext) { - try { - if (html.includes('>')) { - // This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works - return processHtml(html, (string) => { - if (string.includes('>') && - string - .replace(/<[^>]+?>/gi, '') // remove all tags - .replace(/@\w+/gi, '') // remove mentions (even failed ones) - .trim() - .startsWith('>')) { - return `<span class='greentext'>${string}</span>` - } else { - return string - } - }) - } else { - return html - } - } catch (e) { - console.err('Failed to process status html', e) - return html - } - } else { - return html - } - }, ...mapGetters(['mergedConfig']), ...mapState({ - betterShadow: state => state.interface.browserSupport.cssFilter, currentUser: state => state.users.currentUser }) }, @@ -164,52 +69,8 @@ const StatusContent = { Attachment, Poll, Gallery, - LinkPreview - }, - methods: { - linkClicked (event) { - const target = event.target.closest('.status-content a') - if (target) { - if (target.className.match(/mention/)) { - const href = target.href - const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href)) - if (attn) { - event.stopPropagation() - event.preventDefault() - const link = this.generateUserProfileLink(attn.id, attn.screen_name) - this.$router.push(link) - return - } - } - if (target.rel.match(/(?:^|\s)tag(?:$|\s)/) || target.className.match(/hashtag/)) { - // Extract tag name from dataset or link url - const tag = target.dataset.tag || extractTagFromUrl(target.href) - if (tag) { - const link = this.generateTagLink(tag) - this.$router.push(link) - return - } - } - window.open(target.href, '_blank') - } - }, - toggleShowMore () { - if (this.mightHideBecauseTall) { - this.showingTall = !this.showingTall - } else if (this.mightHideBecauseSubject) { - this.expandingSubject = !this.expandingSubject - } - }, - generateUserProfileLink (id, name) { - return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) - }, - generateTagLink (tag) { - return `/tag/${tag}` - }, - setMedia () { - const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments - return () => this.$store.dispatch('setMedia', attachments) - } + LinkPreview, + StatusBody } } diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index 90bfaf40ce3b2155a9a956c2f81f1fdea47697f0..69635aad19d5ac444e36d7ced08e79e49b0de027 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -1,133 +1,53 @@ <template> - <!-- eslint-disable vue/no-v-html --> - <div class="StatusContent"> + <div + class="StatusContent" + :class="{ '-compact': compact }" + > <slot name="header" /> - <div - v-if="status.summary_html" - class="summary-wrapper" - :class="{ 'tall-subject': (longSubject && !showingLongSubject) }" + <StatusBody + :status="status" + :compact="compact" + :single-line="singleLine" + @parseReady="$emit('parseReady', $event)" > - <div - class="media-body summary" - @click.prevent="linkClicked" - v-html="status.summary_html" - /> - <button - v-if="longSubject && showingLongSubject" - class="button-unstyled -link tall-subject-hider" - @click.prevent="showingLongSubject=false" - > - {{ $t("status.hide_full_subject") }} - </button> - <button - v-else-if="longSubject" - class="button-unstyled -link tall-subject-hider" - :class="{ 'tall-subject-hider_focused': focused }" - @click.prevent="showingLongSubject=true" - > - {{ $t("status.show_full_subject") }} - </button> - </div> - <div - :class="{'tall-status': hideTallStatus}" - class="status-content-wrapper" - > - <button - v-if="hideTallStatus" - class="button-unstyled -link tall-status-hider" - :class="{ 'tall-status-hider_focused': focused }" - @click.prevent="toggleShowMore" - > - {{ $t("general.show_more") }} - </button> - <div - v-if="!hideSubjectStatus" - :class="{ 'single-line': singleLine }" - class="status-content media-body" - @click.prevent="linkClicked" - v-html="postBodyHtml" - /> - <button - v-if="hideSubjectStatus" - class="button-unstyled -link cw-status-hider" - @click.prevent="toggleShowMore" - > - {{ $t("status.show_content") }} - <FAIcon - v-if="attachmentTypes.includes('image')" - icon="image" - /> - <FAIcon - v-if="attachmentTypes.includes('video')" - icon="video" - /> - <FAIcon - v-if="attachmentTypes.includes('audio')" - icon="music" - /> - <FAIcon - v-if="attachmentTypes.includes('unknown')" - icon="file" + <div v-if="status.poll && status.poll.options && !compact"> + <Poll + :base-poll="status.poll" + :emoji="status.emojis" /> + </div> + + <div v-else-if="status.poll && status.poll.options && compact"> <FAIcon - v-if="status.poll && status.poll.options" icon="poll-h" + size="2x" /> - <FAIcon - v-if="status.card" - icon="link" - /> - </button> - <button - v-if="showingMore && !fullContent" - class="button-unstyled -link status-unhider" - @click.prevent="toggleShowMore" - > - {{ tallStatus ? $t("general.show_less") : $t("status.hide_content") }} - </button> - </div> - - <div v-if="status.poll && status.poll.options && !hideSubjectStatus"> - <poll :base-poll="status.poll" /> - </div> + </div> - <div - v-if="status.attachments.length !== 0 && (!hideSubjectStatus || showingLongSubject)" - class="attachments media-body" - > - <attachment - v-for="attachment in nonGalleryAttachments" - :key="attachment.id" - class="non-gallery" - :size="attachmentSize" + <gallery + v-if="status.attachments.length !== 0" + class="attachments media-body" :nsfw="nsfwClickthrough" - :attachment="attachment" - :allow-play="true" - :set-media="setMedia()" + :attachments="status.attachments" + :limit="compact ? 1 : 0" + :size="attachmentSize" @play="$emit('mediaplay', attachment.id)" @pause="$emit('mediapause', attachment.id)" /> - <gallery - v-if="galleryAttachments.length > 0" - :nsfw="nsfwClickthrough" - :attachments="galleryAttachments" - :set-media="setMedia()" - /> - </div> - <div - v-if="status.card && !hideSubjectStatus && !noHeading" - class="link-preview media-body" - > - <link-preview - :card="status.card" - :size="attachmentSize" - :nsfw="nsfwClickthrough" - /> - </div> + <div + v-if="status.card && !noHeading && !compact" + class="link-preview media-body" + > + <link-preview + :card="status.card" + :size="attachmentSize" + :nsfw="nsfwClickthrough" + /> + </div> + </StatusBody> <slot name="footer" /> </div> - <!-- eslint-enable vue/no-v-html --> </template> <script src="./status_content.js" ></script> @@ -139,156 +59,5 @@ $status-margin: 0.75em; .StatusContent { flex: 1; min-width: 0; - - .status-content-wrapper { - display: flex; - flex-direction: column; - flex-wrap: nowrap; - } - - .tall-status { - position: relative; - height: 220px; - overflow-x: hidden; - overflow-y: hidden; - z-index: 1; - .status-content { - min-height: 0; - mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat, - linear-gradient(to top, white, white); - /* Autoprefixed seem to ignore this one, and also syntax is different */ - -webkit-mask-composite: xor; - mask-composite: exclude; - } - } - - .tall-status-hider { - display: inline-block; - word-break: break-all; - position: absolute; - height: 70px; - margin-top: 150px; - width: 100%; - text-align: center; - line-height: 110px; - z-index: 2; - } - - .status-unhider, .cw-status-hider { - width: 100%; - text-align: center; - display: inline-block; - word-break: break-all; - - svg { - color: inherit; - } - } - - img, video { - max-width: 100%; - max-height: 400px; - vertical-align: middle; - object-fit: contain; - - &.emoji { - width: 32px; - height: 32px; - } - } - - .summary-wrapper { - margin-bottom: 0.5em; - border-style: solid; - border-width: 0 0 1px 0; - border-color: var(--border, $fallback--border); - flex-grow: 0; - } - - .summary { - font-style: italic; - padding-bottom: 0.5em; - } - - .tall-subject { - position: relative; - .summary { - max-height: 2em; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - } - - .tall-subject-hider { - display: inline-block; - word-break: break-all; - // position: absolute; - width: 100%; - text-align: center; - padding-bottom: 0.5em; - } - - .status-content { - font-family: var(--postFont, sans-serif); - line-height: 1.4em; - white-space: pre-wrap; - overflow-wrap: break-word; - word-wrap: break-word; - word-break: break-word; - - blockquote { - margin: 0.2em 0 0.2em 2em; - font-style: italic; - } - - pre { - overflow: auto; - } - - code, samp, kbd, var, pre { - font-family: var(--postCodeFont, monospace); - } - - p { - margin: 0 0 1em 0; - } - - p:last-child { - margin: 0 0 0 0; - } - - h1 { - font-size: 1.1em; - line-height: 1.2em; - margin: 1.4em 0; - } - - h2 { - font-size: 1.1em; - margin: 1.0em 0; - } - - h3 { - font-size: 1em; - margin: 1.2em 0; - } - - h4 { - margin: 1.1em 0; - } - - &.single-line { - white-space: nowrap; - text-overflow: ellipsis; - overflow: hidden; - height: 1.4em; - } - } -} - -.greentext { - color: $fallback--cGreen; - color: var(--postGreentext, $fallback--cGreen); } </style> diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue index 8237ce00d52d112073166f51e9ea823c2185b040..fdca8c9ce0fdd3b73797f146722c9cb6e6c55f3a 100644 --- a/src/components/status_popover/status_popover.vue +++ b/src/components/status_popover/status_popover.vue @@ -5,12 +5,10 @@ :bound-to="{ x: 'container' }" @show="enter" > - <template slot="trigger"> + <template v-slot:trigger> <slot /> </template> - <div - slot="content" - > + <template v-slot:content> <Status v-if="status" :is-preview="true" @@ -33,7 +31,7 @@ size="2x" /> </div> - </div> + </template> </Popover> </template> diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js index 8044e994365843df2de989c3f2d90c002407ade1..d7abbcb5ee891915556294188d23cd3c5bff547f 100644 --- a/src/components/still-image/still-image.js +++ b/src/components/still-image/still-image.js @@ -5,7 +5,9 @@ const StillImage = { 'mimetype', 'imageLoadError', 'imageLoadHandler', - 'alt' + 'alt', + 'height', + 'width' ], data () { return { @@ -15,6 +17,13 @@ const StillImage = { computed: { animated () { return this.stopGifs && (this.mimetype === 'image/gif' || this.src.endsWith('.gif')) + }, + style () { + const appendPx = (str) => /\d$/.test(str) ? str + 'px' : str + return { + height: this.height ? appendPx(this.height) : null, + width: this.width ? appendPx(this.width) : null + } } }, methods: { diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index d3eb5925c8ee8230fc1fb96d5639cb4d96ed1d6b..cca75fcb74d2c8b457272bc011bfb44e44434c78 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -2,6 +2,7 @@ <div class="still-image" :class="{ animated: animated }" + :style="style" > <canvas v-if="animated" @@ -30,7 +31,7 @@ position: relative; line-height: 0; overflow: hidden; - display: flex; + display: inline-flex; align-items: center; canvas { @@ -47,12 +48,13 @@ img { width: 100%; - min-height: 100%; + height: 100%; object-fit: contain; } &.animated { &::before { + zoom: var(--_still_image-label-scale, 1); content: 'gif'; position: absolute; line-height: 10px; diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js index 76e7ef03d8714d5d9199aca7c30cea680d14ebfd..12aac8e66749a68e5e0884edd274dfc94822db2b 100644 --- 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 index 6df0524db7b1f07093008e9f4a6083932964080e..55a2dd9451466415e45f4393f25cc44ae030cdba 100644 --- 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 index 665d195ea18ff241c99d595d44f6b90881f74639..04f0e7d6be572843e2ade232ffa39c1f8fd4744c 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -2,27 +2,16 @@ 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) => { - const ids = [] - if (pinnedStatusIds && pinnedStatusIds.length > 0) { - for (let status of statuses) { - if (!pinnedStatusIds.includes(status.id)) { - break - } - ids.push(status.id) - } - } - return ids -} - const Timeline = { props: [ 'timeline', @@ -47,7 +36,8 @@ const Timeline = { components: { Status, Conversation, - TimelineMenu + TimelineMenu, + TimelineQuickSettings }, computed: { newStatusCount () { @@ -74,11 +64,6 @@ const Timeline = { } }, // id map of statuses which need to be hidden in the main list due to pinning logic - excludedStatusIdsObject () { - const ids = getExcludedStatusIdsByPinning(this.timeline.visibleStatuses, this.pinnedStatusIds) - // Convert id array to object - return keyBy(ids) - }, pinnedStatusIdsObject () { return keyBy(this.pinnedStatusIds) }, diff --git a/src/components/timeline/timeline.scss b/src/components/timeline/timeline.scss new file mode 100644 index 0000000000000000000000000000000000000000..2c5a67e2b73fa5bb24fb50cf1ff1c9336562d3aa --- /dev/null +++ b/src/components/timeline/timeline.scss @@ -0,0 +1,31 @@ +@import '../../_variables.scss'; + +.Timeline { + .loadmore-text { + opacity: 1; + } + + &.-blocked { + cursor: progress; + } + + .timeline-heading { + max-width: 100%; + flex-wrap: nowrap; + align-items: center; + position: relative; + + .loadmore-button { + flex-shrink: 0; + } + + .loadmore-text { + flex-shrink: 0; + line-height: 1em; + } + } + + .timeline-footer { + border: none; + } +} diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 4c43fe5ce8c509bfd6f9409bab6a7d827820a728..ff16208d987b10fbbf6fa6cbe8846e50123b1cbb 100644 --- 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 @@ -36,7 +37,7 @@ </template> <template v-for="status in timeline.visibleStatuses"> <conversation - v-if="!excludedStatusIdsObject[status.id]" + v-if="timelineName !== 'user' || (status.id >= timeline.minId && status.id <= timeline.maxId)" :key="status.id" class="status-fadein" :status-id="status.id" @@ -51,13 +52,13 @@ <div :class="classes.footer"> <div v-if="count===0" - class="new-status-notification text-center panel-footer faint" + class="new-status-notification text-center faint" > {{ $t('timeline.no_statuses') }} </div> <div v-else-if="bottomedOut" - class="new-status-notification text-center panel-footer faint" + class="new-status-notification text-center faint" > {{ $t('timeline.no_more_statuses') }} </div> @@ -66,13 +67,13 @@ class="button-unstyled -link -fullwidth" @click.prevent="fetchOlderStatuses()" > - <div class="new-status-notification text-center panel-footer"> + <div class="new-status-notification text-center"> {{ $t('timeline.load_older') }} </div> </button> <div v-else - class="new-status-notification text-center panel-footer" + class="new-status-notification text-center" > <FAIcon icon="circle-notch" @@ -86,29 +87,4 @@ <script src="./timeline.js"></script> -<style lang="scss"> -@import '../../_variables.scss'; - -.Timeline { - .loadmore-text { - opacity: 1; - } - - &.-blocked { - cursor: progress; - } -} - -.timeline-heading { - max-width: 100%; - flex-wrap: nowrap; - align-items: center; - .loadmore-button { - flex-shrink: 0; - } - .loadmore-text { - flex-shrink: 0; - line-height: 1em; - } -} -</style> +<style src="./timeline.scss" lang="scss"> </style> diff --git a/src/components/timeline/timeline_quick_settings.js b/src/components/timeline/timeline_quick_settings.js new file mode 100644 index 0000000000000000000000000000000000000000..7b4931ce6a72d132d8462f913a24bd4f8492d011 --- /dev/null +++ b/src/components/timeline/timeline_quick_settings.js @@ -0,0 +1,60 @@ +import Popover from '../popover/popover.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 + }, + 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.hideFilteredStatuses }, + set () { + const value = !this.hideMutedPosts + 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 new file mode 100644 index 0000000000000000000000000000000000000000..98996ebda22b6e62a787896273c0a9a5182ee85a --- /dev/null +++ b/src/components/timeline/timeline_quick_settings.vue @@ -0,0 +1,102 @@ +<template> + <Popover + trigger="click" + class="TimelineQuickSettings" + :bound-to="{ x: 'container' }" + > + <template v-slot:content> + <div class="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> + </template> + <template v-slot:trigger> + <button class="button-unstyled"> + <FAIcon icon="filter" /> + </button> + </template> + </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; + } +} + +</style> diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index 8d6a58b11e913bfe61b191f9fe2c2d6efb98dab5..bab51e75e2d20da93bf1a29754f90be5a2938b07 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -1,29 +1,17 @@ import Popover from '../popover/popover.vue' -import { mapState } from 'vuex' +import TimelineMenuContent from './timeline_menu_content.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome, faChevronDown } from '@fortawesome/free-solid-svg-icons' -library.add( - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome, - faChevronDown -) +library.add(faChevronDown) // Route -> i18n key mapping, exported and not in the computed // because nav panel benefits from the same information. export const timelineNames = () => { return { - 'friends': 'nav.timeline', + 'friends': 'nav.home_timeline', 'bookmarks': 'nav.bookmarks', 'dms': 'nav.dms', 'public-timeline': 'nav.public_tl', @@ -33,7 +21,8 @@ export const timelineNames = () => { const TimelineMenu = { components: { - Popover + Popover, + TimelineMenuContent }, data () { return { @@ -41,9 +30,6 @@ const TimelineMenu = { } }, created () { - if (this.currentUser && this.currentUser.locked) { - this.$store.dispatch('startFetchingFollowRequests') - } if (timelineNames()[this.$route.name]) { this.$store.dispatch('setLastTimeline', this.$route.name) } @@ -75,13 +61,6 @@ const TimelineMenu = { const i18nkey = timelineNames()[this.$route.name] return i18nkey ? this.$t(i18nkey) : route } - }, - computed: { - ...mapState({ - currentUser: state => state.users.currentUser, - privateMode: state => state.instance.private, - federating: state => state.instance.federating - }) } } diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue index 3c86842b7d8f3ab4cf7a476c1751da649e4e2b2c..8f14093f05cadb94ff35c8126f6cdfe67eeef30f 100644 --- a/src/components/timeline_menu/timeline_menu.vue +++ b/src/components/timeline_menu/timeline_menu.vue @@ -9,74 +9,26 @@ @show="openMenu" @close="() => isOpen = false" > - <div - slot="content" - class="timeline-menu-popover panel panel-default" - > - <ul> - <li v-if="currentUser"> - <router-link :to="{ name: 'friends' }"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="home" - />{{ $t("nav.timeline") }} - </router-link> - </li> - <li v-if="currentUser"> - <router-link :to="{ name: 'bookmarks'}"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="bookmark" - />{{ $t("nav.bookmarks") }} - </router-link> - </li> - <li v-if="currentUser"> - <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="envelope" - />{{ $t("nav.dms") }} - </router-link> - </li> - <li v-if="currentUser || !privateMode"> - <router-link :to="{ name: 'public-timeline' }"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="users" - />{{ $t("nav.public_tl") }} - </router-link> - </li> - <li v-if="federating && (currentUser || !privateMode)"> - <router-link :to="{ name: 'public-external-timeline' }"> - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="globe" - />{{ $t("nav.twkn") }} - </router-link> - </li> - </ul> - </div> - <div - slot="trigger" - class="title timeline-menu-title" - > - <span class="timeline-title">{{ timelineName() }}</span> - <span> - <FAIcon - size="sm" - icon="chevron-down" + <template v-slot:content> + <div class="timeline-menu-popover popover-default"> + <TimelineMenuContent /> + </div> + </template> + <template v-slot:trigger> + <button class="button-unstyled title timeline-menu-title"> + <span class="timeline-title">{{ timelineName() }}</span> + <span> + <FAIcon + size="sm" + icon="chevron-down" + /> + </span> + <span + class="click-blocker" + @click="blockOpen" /> - </span> - <span - class="click-blocker" - @click="blockOpen" - /> - </div> + </button> + </template> </Popover> </template> diff --git a/src/components/timeline_menu/timeline_menu_content.js b/src/components/timeline_menu/timeline_menu_content.js new file mode 100644 index 0000000000000000000000000000000000000000..671570ddf7d6bb1729021e5b11160122073cdf24 --- /dev/null +++ b/src/components/timeline_menu/timeline_menu_content.js @@ -0,0 +1,29 @@ +import { mapState } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome +) + +const TimelineMenuContent = { + computed: { + ...mapState({ + currentUser: state => state.users.currentUser, + privateMode: state => state.instance.private, + federating: state => state.instance.federating + }) + } +} + +export default TimelineMenuContent diff --git a/src/components/timeline_menu/timeline_menu_content.vue b/src/components/timeline_menu/timeline_menu_content.vue new file mode 100644 index 0000000000000000000000000000000000000000..bed1b679a4fddd1d4032235681b85faed22df98c --- /dev/null +++ b/src/components/timeline_menu/timeline_menu_content.vue @@ -0,0 +1,66 @@ +<template> + <ul> + <li v-if="currentUser"> + <router-link + class="menu-item" + :to="{ name: 'friends' }" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="home" + />{{ $t("nav.home_timeline") }} + </router-link> + </li> + <li v-if="currentUser || !privateMode"> + <router-link + class="menu-item" + :to="{ name: 'public-timeline' }" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="users" + />{{ $t("nav.public_tl") }} + </router-link> + </li> + <li v-if="federating && (currentUser || !privateMode)"> + <router-link + class="menu-item" + :to="{ name: 'public-external-timeline' }" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="globe" + />{{ $t("nav.twkn") }} + </router-link> + </li> + <li v-if="currentUser"> + <router-link + class="menu-item" + :to="{ name: 'bookmarks'}" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="bookmark" + />{{ $t("nav.bookmarks") }} + </router-link> + </li> + <li v-if="currentUser"> + <router-link + class="menu-item" + :to="{ name: 'dms', params: { username: currentUser.screen_name } }" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="envelope" + />{{ $t("nav.dms") }} + </router-link> + </li> + </ul> +</template> + +<script src="./timeline_menu_content.js" ></script> diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue index 0f7c584b13ac45bdd38a68b9c3aea739ebebda38..4040e263c8d4daeffd792fe8882a70f6a98ed5ca 100644 --- 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.js b/src/components/user_card/user_card.js index 3a8efafc910263384d9bbdf9bc3f9ad08285f445..4168c54ae1c8afacb4804a9ac2456e8766939c8d 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -4,23 +4,25 @@ import ProgressButton from '../progress_button/progress_button.vue' import FollowButton from '../follow_button/follow_button.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue' import AccountActions from '../account_actions/account_actions.vue' +import Select from '../select/select.vue' +import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { faBell, faRss, - faChevronDown, faSearchPlus, - faExternalLinkAlt + faExternalLinkAlt, + faEdit } from '@fortawesome/free-solid-svg-icons' library.add( faRss, faBell, - faChevronDown, faSearchPlus, - faExternalLinkAlt + faExternalLinkAlt, + faEdit ) export default { @@ -118,7 +120,9 @@ export default { ModerationTools, AccountActions, ProgressButton, - FollowButton + FollowButton, + Select, + RichContent }, methods: { muteUser () { @@ -153,13 +157,16 @@ export default { this.$store.state.instance.restrictedNicknames ) }, + openProfileTab () { + this.$store.dispatch('openSettingsModalTab', 'profile') + }, zoomAvatar () { const attachment = { url: this.user.profile_image_url_original, mimetype: 'image' } this.$store.dispatch('setMedia', [attachment]) - this.$store.dispatch('setCurrent', attachment) + this.$store.dispatch('setCurrentMedia', attachment) }, mentionUser () { this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user }) diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 773f764a0ebfbeb214968df08d437e8ab55798f9..0708f38749a4945faf0502c0eabe3d8a8f134b9d 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -38,26 +38,29 @@ </router-link> <div class="user-summary"> <div class="top-line"> - <!-- eslint-disable vue/no-v-html --> - <div - v-if="user.name_html" + <RichContent :title="user.name" class="user-name" - v-html="user.name_html" + :html="user.name" + :emoji="user.emoji" /> - <!-- eslint-enable vue/no-v-html --> - <div - v-else - :title="user.name" - class="user-name" + <button + v-if="!isOtherUser && user.is_local" + class="button-unstyled edit-profile-button" + @click.stop="openProfileTab" > - {{ user.name }} - </div> + <FAIcon + fixed-width + class="icon" + icon="edit" + :title="$t('user_card.edit_profile')" + /> + </button> <a v-if="isOtherUser && !user.is_local" :href="user.statusnet_profile_url" target="_blank" - class="external-link-button" + class="button-unstyled external-link-button" > <FAIcon class="icon" @@ -73,23 +76,29 @@ <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="user.deactivated" + class="alert user-role" + > + {{ $t('user_card.deactivated') }} + </span> <span v-if="!!visibleRole" class="alert user-role" > - {{ $t(`user_card.roles.${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"> @@ -132,25 +141,24 @@ class="userHighlightCl" type="color" > - <label - for="theme_tab" - class="userHighlightSel select" + <Select + :id="'userHighlightSel'+user.id" + v-model="userHighlightType" + class="userHighlightSel" > - <select - :id="'userHighlightSel'+user.id" - 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> - </select> - <FAIcon - class="select-down-icon" - icon="chevron-down" - /> - </label> + <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> </div> </div> <div @@ -158,7 +166,10 @@ class="user-interactions" > <div class="btn-group"> - <FollowButton :relationship="relationship" /> + <FollowButton + :relationship="relationship" + :user="user" + /> <template v-if="relationship.following"> <ProgressButton v-if="!relationship.subscribing" @@ -193,6 +204,7 @@ <button v-if="relationship.muting" class="btn button-default btn-block toggled" + :disabled="user.deactivated" @click="unmuteUser" > {{ $t('user_card.muted') }} @@ -200,6 +212,7 @@ <button v-else class="btn button-default btn-block" + :disabled="user.deactivated" @click="muteUser" > {{ $t('user_card.mute') }} @@ -208,6 +221,7 @@ <div> <button class="btn button-default btn-block" + :disabled="user.deactivated" @click="mentionUser" > {{ $t('user_card.mention') }} @@ -256,20 +270,13 @@ <span>{{ hideFollowersCount ? $t('user_card.hidden') : user.followers_count }}</span> </div> </div> - <!-- eslint-disable vue/no-v-html --> - <p - v-if="!hideBio && user.description_html" + <RichContent + v-if="!hideBio" class="user-card-bio" - @click.prevent="linkClicked" - v-html="user.description_html" + :html="user.description_html" + :emoji="user.emoji" + :handle-links="true" /> - <!-- eslint-enable vue/no-v-html --> - <p - v-else-if="!hideBio" - class="user-card-bio" - > - {{ user.description }} - </p> </div> </div> </template> @@ -282,9 +289,10 @@ .user-card { position: relative; - &:hover .Avatar { + &:hover { --_still-image-img-visibility: visible; --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; } .panel-heading { @@ -328,12 +336,12 @@ } } - p { - margin-bottom: 0; - } - &-bio { text-align: center; + display: block; + line-height: 18px; + padding: 1em; + margin: 0; a { color: $fallback--link; @@ -345,11 +353,6 @@ vertical-align: middle; max-width: 100%; max-height: 400px; - - &.emoji { - width: 32px; - height: 32px; - } } } @@ -427,7 +430,7 @@ } } - .external-link-button { + .external-link-button, .edit-profile-button { cursor: pointer; width: 2.5em; text-align: center; @@ -451,13 +454,6 @@ // big one z-index: 1; - img { - width: 26px; - height: 26px; - vertical-align: middle; - object-fit: contain - } - .top-line { display: flex; } @@ -470,12 +466,7 @@ margin-right: 1em; font-size: 15px; - img { - object-fit: contain; - height: 16px; - width: 16px; - vertical-align: middle; - } + --emoji-size: 14px; } .bottom-line { @@ -541,15 +532,11 @@ flex: 1 0 auto; } - .userHighlightSel, - .userHighlightSel.select { + .userHighlightSel { padding-top: 0; padding-bottom: 0; flex: 1 0 auto; } - .userHighlightSel.select svg { - line-height: 22px; - } .userHighlightText { width: 70px; @@ -558,9 +545,7 @@ .userHighlightCl, .userHighlightText, - .userHighlightSel, - .userHighlightSel.select { - height: 22px; + .userHighlightSel { vertical-align: top; margin-right: .5em; margin-bottom: .25em; @@ -585,6 +570,10 @@ } } +.sidebar .edit-profile-button { + display: none; +} + .user-counts { display: flex; line-height:16px; diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue index 95673733e3d169b9fe2ebc7574e588df2c313a89..8706d0ff6b28343f3d24c0aa83ed03165d0c7cec 100644 --- a/src/components/user_list_popover/user_list_popover.vue +++ b/src/components/user_list_popover/user_list_popover.vue @@ -4,40 +4,44 @@ placement="top" :offset="{ y: 5 }" > - <template slot="trigger"> + <template v-slot:trigger> <slot /> </template> - <div - slot="content" - class="user-list-popover" - > - <div v-if="users.length"> - <div - v-for="(user) in usersCapped" - :key="user.id" - class="user-list-row" - > - <UserAvatar - :user="user" - class="avatar-small" - :compact="true" - /> - <div class="user-list-names"> - <!-- 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> + <template v-slot:content> + <div class="user-list-popover"> + <template v-if="users.length"> + <div + v-for="(user) in usersCapped" + :key="user.id" + class="user-list-row" + > + <UserAvatar + :user="user" + class="avatar-small" + :compact="true" + /> + <div class="user-list-names"> + <!-- eslint-disable vue/no-v-html --> + <RichContent + class="username" + :title="'@'+user.screen_name_ui" + :html="user.name_html" + :emoji="user.emoji" + /> + <!-- eslint-enable vue/no-v-html --> + <span class="user-list-screen-name">{{ user.screen_name_ui }}</span> + </div> </div> - </div> - </div> - <div v-else> - <FAIcon - icon="circle-notch" - spin - size="3x" - /> + </template> + <template v-else> + <FAIcon + icon="circle-notch" + spin + size="3x" + /> + </template> </div> - </div> + </template> </Popover> </template> @@ -49,6 +53,8 @@ .user-list-popover { padding: 0.5em; + --emoji-size: 16px; + .user-list-row { padding: 0.25em; display: flex; diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index c0b55a6ceca12941363eb6937cd5c5eaa1aa12a8..7a47560916614f010c7c12b5e7425bb463b61675 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -4,6 +4,7 @@ import FollowCard from '../follow_card/follow_card.vue' import Timeline from '../timeline/timeline.vue' import Conversation from '../conversation/conversation.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' +import RichContent from 'src/components/rich_content/rich_content.jsx' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' import { library } from '@fortawesome/fontawesome-svg-core' @@ -164,7 +165,8 @@ const UserProfile = { FriendList, FollowCard, TabSwitcher, - Conversation + Conversation, + RichContent } } diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 745e795decc524c9c8137a2646b9a6c8aff7f50d..726216ff034aca2b55fc0d68ad53b16371fc4fbf 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -20,20 +20,24 @@ :key="index" class="user-profile-field" > - <!-- eslint-disable vue/no-v-html --> <dt :title="user.fields_text[index].name" class="user-profile-field-name" - @click.prevent="linkClicked" - v-html="field.name" - /> + > + <RichContent + :html="field.name" + :emoji="user.emoji" + /> + </dt> <dd :title="user.fields_text[index].value" class="user-profile-field-value" - @click.prevent="linkClicked" - v-html="field.value" - /> - <!-- eslint-enable vue/no-v-html --> + > + <RichContent + :html="field.value" + :emoji="user.emoji" + /> + </dd> </dl> </div> <tab-switcher @@ -60,10 +64,7 @@ :disabled="!user.friends_count" > <FriendList :user-id="userId"> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <FollowCard :user="item" /> </template> </FriendList> @@ -75,10 +76,7 @@ :disabled="!user.followers_count" > <FollowerList :user-id="userId"> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <FollowCard :user="item" :no-follows-you="isUs" diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue index fb43094f188cf56e8591c62b3dc3e3bf0cb83b59..1f67a5cc6ef69abc762ab736351ba25dbb7894d2 100644 --- 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"> @@ -45,10 +45,7 @@ </div> <div class="user-reporting-panel-right"> <List :items="statuses"> - <template - slot="item" - slot-scope="{item}" - > + <template v-slot:item="{item}"> <div class="status-fadein user-reporting-panel-sitem"> <Status :in-conversation="false" diff --git a/src/i18n/ca.json b/src/i18n/ca.json index b15b69f7c69887c92e4a3167038a53bcbf9fedca..74260143a798c6b51403e4a15e5a66f418b482aa 100644 --- a/src/i18n/ca.json +++ b/src/i18n/ca.json @@ -10,11 +10,12 @@ "text_limit": "LÃmit de text", "title": "Funcionalitats", "who_to_follow": "A qui seguir", - "pleroma_chat_messages": "Xat de Pleroma" + "pleroma_chat_messages": "Xat de Pleroma", + "upload_limit": "LÃmit de cà rrega" }, "finder": { "error_fetching_user": "No s'ha pogut carregar l'usuari/a", - "find_user": "Find user" + "find_user": "Trobar usuari" }, "general": { "apply": "Aplica", @@ -32,7 +33,16 @@ "error_retry": "Si us plau, prova de nou", "generic_error": "Hi ha hagut un error", "loading": "Carregant…", - "more": "Més" + "more": "Més", + "flash_content": "Fes clic per mostrar el contingut Flash utilitzant Ruffle (experimental, pot no funcionar).", + "flash_security": "Tingues en compte que això pot ser potencialment perillós, ja que el contingut Flash encara és un codi arbitrari.", + "flash_fail": "No s'ha pogut carregar el contingut del flaix, consulta la consola per als detalls.", + "role": { + "moderator": "Moderador/a", + "admin": "Administrador/a" + }, + "dismiss": "Descartar", + "peek": "Donar un cop d'ull" }, "login": { "login": "Inicia sessió", @@ -45,15 +55,20 @@ "enter_recovery_code": "Posa un codi de recuperació", "authentication_code": "Codi d'autenticació", "hint": "Entra per participar a la conversa", - "description": "Entra amb OAuth" + "description": "Entra amb OAuth", + "heading": { + "totp": "Autenticació de dos factors", + "recovery": "Recuperació de dos factors" + }, + "enter_two_factor_code": "Introdueix un codi de dos factors" }, "nav": { "chat": "Xat local públic", - "friend_requests": "SoÅ€licituds de connexió", + "friend_requests": "Sol·licituds de seguiment", "mentions": "Mencions", - "public_tl": "Flux públic del node", + "public_tl": "LÃnia temporal pública", "timeline": "Flux personal", - "twkn": "Flux de la xarxa coneguda", + "twkn": "Xarxa coneguda", "chats": "Xats", "timelines": "LÃnies de temps", "preferences": "Preferències", @@ -62,19 +77,25 @@ "dms": "Missatges directes", "interactions": "Interaccions", "back": "Enrere", - "administration": "Administració" + "administration": "Administració", + "about": "Quant a", + "bookmarks": "Marcadors", + "user_search": "Cerca d'usuaris", + "home_timeline": "LÃnea temporal personal" }, "notifications": { - "broken_favorite": "No es coneix aquest estat. S'està cercant.", + "broken_favorite": "Publicació desconeguda, s'està cercant…", "favorited_you": "ha marcat un estat teu", "followed_you": "ha començat a seguir-te", "load_older": "Carrega més notificacions", "notifications": "Notificacions", - "read": "Read!", + "read": "Llegit!", "repeated_you": "ha repetit el teu estat", "migrated_to": "migrat a", "no_more_notifications": "No més notificacions", - "follow_request": "et vol seguir" + "follow_request": "et vol seguir", + "reacted_with": "ha reaccionat amb {0}", + "error": "Error obtenint notificacions: {0}" }, "post_status": { "account_not_locked_warning": "El teu compte no està {0}. Qualsevol persona pot seguir-te per llegir les teves entrades reservades només a seguidores.", @@ -83,24 +104,33 @@ "content_type": { "text/plain": "Text pla", "text/markdown": "Markdown", - "text/html": "HTML" + "text/html": "HTML", + "text/bbcode": "BBCode" }, "content_warning": "Assumpte (opcional)", - "default": "Em sento…", + "default": "Acabe d'aterrar a L.A.", "direct_warning": "Aquesta entrada només serà visible per les usurà ries que etiquetis", "posting": "Publicació", "scope": { - "direct": "Directa - Publica només per les usuà ries etiquetades", - "private": "Només seguidors/es - Publica només per comptes que et segueixin", - "public": "Pública - Publica als fluxos públics", - "unlisted": "Silenciosa - No la mostris en fluxos públics" + "direct": "Directa - publica només per als usuaris etiquetats", + "private": "Només seguidors/es - publica només per comptes que et segueixin", + "public": "Pública - publica als fluxos públics", + "unlisted": "Silenciosa - no la mostris en fluxos públics" }, "scope_notice": { "private": "Aquesta entrada serà visible només per a qui et segueixi", - "public": "Aquesta entrada serà visible per a tothom" + "public": "Aquesta entrada serà visible per a tothom", + "unlisted": "Aquesta entrada no es veurà ni a la LÃnia de temps local ni a la LÃnia de temps federada" }, "preview_empty": "Buida", - "preview": "Vista prèvia" + "preview": "Vista prèvia", + "direct_warning_to_first_only": "Aquesta publicació només serà visible per als usuaris mencionats al principi del missatge.", + "empty_status_error": "No es pot publicar un estat buit sense fitxers adjunts", + "media_description": "Descripció multimèdia", + "direct_warning_to_all": "Aquesta publicació serà visible per a tots els usuaris mencionats.", + "new_status": "Publicar un nou estat", + "post": "Publicació", + "media_description_error": "Ha fallat la pujada del contingut. Prova de nou" }, "registration": { "bio": "Presentació", @@ -118,13 +148,19 @@ "username_required": "no es pot deixar en blanc" }, "fullname_placeholder": "p. ex. Lain Iwakura", - "username_placeholder": "p. ex. lain" + "username_placeholder": "p. ex. lain", + "captcha": "CAPTCHA", + "register": "Registrar-se", + "reason": "Raó per a registrar-se", + "bio_placeholder": "p.e.\nHola, sóc la Lain.\nSóc una noia anime que viu a un suburbi de Japó. Potser em coneixes per Wired.", + "reason_placeholder": "Aquesta instà ncia aprova els registres manualment.\nExplica a l'administració per què vols registrar-te.", + "new_captcha": "Clica a la imatge per obtenir un nou captcha" }, "settings": { "attachmentRadius": "Adjunts", "attachments": "Adjunts", "avatar": "Avatar", - "avatarAltRadius": "Avatars en les notificacions", + "avatarAltRadius": "Avatars (notificacions)", "avatarRadius": "Avatars", "background": "Fons de pantalla", "bio": "Presentació", @@ -134,8 +170,8 @@ "cOrange": "Taronja (marca com a preferit)", "cRed": "Vermell (canceÅ€la)", "change_password": "Canvia la contrasenya", - "change_password_error": "No s'ha pogut canviar la contrasenya", - "changed_password": "S'ha canviat la contrasenya", + "change_password_error": "No s'ha pogut canviar la contrasenya.", + "changed_password": "S'ha canviat la contrasenya correctament!", "collapse_subject": "Replega les entrades amb tÃtol", "confirm_new_password": "Confirma la nova contrasenya", "current_avatar": "L'avatar actual", @@ -176,7 +212,7 @@ "new_password": "Contrasenya nova", "notification_visibility": "Notifica'm quan algú", "notification_visibility_follows": "Comença a seguir-me", - "notification_visibility_likes": "Marca com a preferida una entrada meva", + "notification_visibility_likes": "Favorits", "notification_visibility_mentions": "Em menciona", "notification_visibility_repeats": "Republica una entrada meva", "no_rich_text_description": "Neteja el formatat de text de totes les entrades", @@ -193,7 +229,7 @@ "profile_banner": "Fons de perfil", "profile_tab": "Perfil", "radii_help": "Configura l'arrodoniment de les vores (en pÃxels)", - "replies_in_timeline": "Replies in timeline", + "replies_in_timeline": "Respostes al flux", "reply_visibility_all": "Mostra totes les respostes", "reply_visibility_following": "Mostra només les respostes a entrades meves o d'usuà ries que jo segueixo", "reply_visibility_self": "Mostra només les respostes a entrades meves", @@ -216,7 +252,7 @@ "true": "sÃ" }, "show_moderator_badge": "Mostra una insÃgnia de Moderació en el meu perfil", - "show_admin_badge": "Mostra una insÃgnia d'Administració en el meu perfil", + "show_admin_badge": "Mostra una insÃgnia \"d'Administració\" en el meu perfil", "hide_followers_description": "No mostris qui m'està seguint", "hide_follows_description": "No mostris a qui segueixo", "notification_visibility_emoji_reactions": "Reaccions", @@ -254,25 +290,270 @@ "allow_following_move": "Permet el seguiment automà tic quan un compte a qui seguim es mou", "mfa": { "scan": { - "secret_code": "Clau" + "secret_code": "Clau", + "title": "Escanejar", + "desc": "S'està usant l'aplicació two-factor, escaneja aquest codi QR o introdueix la clau de text:" }, "authentication_methods": "Mètodes d'autenticació", "waiting_a_recovery_codes": "Rebent còpies de seguretat dels codis…", "recovery_codes": "Codis de recuperació.", "warning_of_generate_new_codes": "Quan generes nous codis de recuperació, els antics ja no funcionaran més.", - "generate_new_recovery_codes": "Genera nous codis de recuperació" + "generate_new_recovery_codes": "Genera nous codis de recuperació", + "otp": "OTP", + "confirm_and_enable": "Confirmar i habilitar OTP", + "recovery_codes_warning": "Anote els codis o guarda'ls en un lloc segur, o no els veurà s una altra volta. Si perds l'accés a la teua aplicació 2FA i els codis de recuperació, no podrà s accedir al compte.", + "title": "Autenticació de dos factors", + "setup_otp": "Configurar OTP", + "wait_pre_setup_otp": "preconfiguració OTP", + "verify": { + "desc": "Per habilitar l'autenticació two-factor, introdueix el codi des de la teva aplicació two-factor:" + } }, "enter_current_password_to_confirm": "Posar la contrasenya actual per confirmar la teva identitat", "security": "Seguretat", - "app_name": "Nom de l'aplicació" + "app_name": "Nom de l'aplicació", + "subject_line_mastodon": "Com a mastodon: copiar com és", + "mute_export_button": "Exportar silenciats a un fitxer csv", + "mute_import_error": "Error al importar silenciats", + "mutes_imported": "Silenciats importats! Processar-los portarà una estona.", + "import_mutes_from_a_csv_file": "Importar silenciats des d'un fitxer csv", + "word_filter": "Filtre de paraules", + "hide_media_previews": "Ocultar les vistes prèvies multimèdia", + "hide_filtered_statuses": "Amagar estats filtrats", + "play_videos_in_modal": "Reproduir vÃdeos en un marc emergent", + "file_export_import": { + "errors": { + "invalid_file": "El fitxer seleccionat no és và lid com a còpia de seguretat de la configuració. No s'ha realitzat cap canvi.", + "file_too_new": "Versió important incompatible: {fileMajor}, aquest PleromaFE (configuració versió {feMajor}) és massa antiga per gestionar-lo", + "file_too_old": "Versió important incompatible: {fileMajor}, la versió del fitxer és massa antiga i no està implementada (s'ha establert un mÃnim ver. {feMajor})", + "file_slightly_new": "La versió menor del fitxer és diferent, alguns parà metres podrien no carregar-se" + }, + "backup_settings": "Còpia de seguretat de la configuració a un fitxer", + "backup_settings_theme": "Còpia de seguretat de la configuració i tema a un fitxer", + "restore_settings": "Restaurar configuració des d'un fitxer", + "backup_restore": "Còpia de seguretat de la configuració" + }, + "user_mutes": "Usuaris", + "subject_line_email": "Com a l'email: \"re: tema\"", + "search_user_to_block": "Busca a qui vols bloquejar", + "save": "Guardar els canvis", + "use_contain_fit": "No retallar els adjunts en miniatures", + "reset_profile_background": "Restablir fons del perfil", + "reset_profile_banner": "Restablir banner del perfil", + "emoji_reactions_on_timeline": "Mostrar reaccions emoji al flux", + "max_thumbnails": "Quantitat mà xima de miniatures per publicació", + "hide_user_stats": "Amagar les estadÃstiques de l'usuari (p. ex. el nombre de seguidors)", + "reset_banner_confirm": "Realment vols restablir el banner?", + "reset_background_confirm": "Realment vols restablir el fons del perfil?", + "subject_input_always_show": "Sempre mostrar el camp del tema", + "subject_line_noop": "No copiar", + "subject_line_behavior": "Copiar el tema a les respostes", + "search_user_to_mute": "Busca a qui vols silenciar", + "mute_export": "Exportar silenciats", + "scope_copy": "Copiar visibilitat quan contestes (En els missatges directes sempre es copia)", + "reset_avatar": "Restablir avatar", + "right_sidebar": "Mostrar barra lateral a la dreta", + "no_blocks": "No hi han bloquejats", + "no_mutes": "No hi han silenciats", + "hide_follows_count_description": "No mostrar el nombre de comptes que segueixo", + "mute_import": "Importar silenciats", + "hide_all_muted_posts": "Ocultar publicacions silenciades", + "hide_wallpaper": "Amagar el fons de la instà ncia", + "notification_visibility_moves": "Usuari Migrat", + "reply_visibility_following_short": "Mostrar respostes als meus seguidors", + "reply_visibility_self_short": "Mostrar respostes només a un mateix", + "autohide_floating_post_button": "Ocultar automà ticament el botó 'Nova Publicació' (mòbil)", + "minimal_scopes_mode": "Minimitzar les opcions de visibilitat de la publicació", + "sensitive_by_default": "Marcar publicacions com a sensibles per defecte", + "useStreamingApi": "Rebre publicacions i notificacions en temps real", + "hide_isp": "Ocultar el panell especific de la instà ncia", + "preload_images": "Precarregar les imatges", + "setting_changed": "La configuració és diferent a la predeterminada", + "hide_followers_count_description": "No mostrar el nombre de seguidors", + "reset_avatar_confirm": "Realment vols restablir l'avatar?", + "accent": "Accent", + "useStreamingApiWarning": "(No recomanat, experimental, pot ometre publicacions)", + "style": { + "fonts": { + "family": "Nom de la font", + "size": "Mida (en pÃxels)", + "custom": "Personalitza", + "_tab_label": "Fonts", + "help": "Selecciona la font per als elements de la interfÃcie. Per a \"personalitzat\" deus escriure el nom de la font exactament com apareix al sistema.", + "components": { + "post": "Text de les publicacions", + "postCode": "Text monoespai en publicació (text enriquit)", + "input": "Camps d'entrada", + "interface": "InterfÃcie" + }, + "weight": "Pes (negreta)" + }, + "preview": { + "input": "Acabo d'aterrar a Los Angeles.", + "button": "Botó", + "mono": "contingut", + "content": "Contingut", + "header": "Previsualització", + "header_faint": "Això està bé", + "error": "Exemple d'error", + "faint_link": "Manual d'ajuda", + "checkbox": "He llegit els termes i condicions", + "link": "un bonic enllaç", + "fine_print": "Llegiu el nostre {0} per no aprendre res útil!", + "text": "Un grapat més de {0} i {1}" + }, + "shadows": { + "spread": "Difon", + "filter_hint": { + "drop_shadow_syntax": "{0} no suporta el parà metre {1} i la paraula clau {2}.", + "avatar_inset": "Tingues en compte que combinar ombres interiors i no interiors als avatars podria donar resultats inesperats amb avatars transparents.", + "inset_classic": "Les ombres interiors estaran usant {0}", + "always_drop_shadow": "Advertència, aquesta ombra sempre utilitza {0} quan el navegador ho suporta.", + "spread_zero": "Ombres amb propagació > 0 apareixeran com si estigueren posades a zero" + }, + "components": { + "popup": "Texts i finestres emergents (popups & tooltips)", + "panel": "Panell", + "panelHeader": "Capçalera del panell", + "avatar": "Avatar de l'usuari (en vista de perfil)", + "input": "Camp d'entrada", + "buttonHover": "Botó (surant)", + "buttonPressed": "Botó (pressionat)", + "topBar": "Barra superior", + "buttonPressedHover": "Botó (surant i pressionat)", + "avatarStatus": "Avatar de l'usuari (en vista de publicació)", + "button": "Botó" + }, + "hintV3": "per a les ombres també pots usar la notació {0} per a utilitzar un altre espai de color.", + "blur": "Difuminat", + "component": "Component", + "override": "Sobreescriure", + "shadow_id": "Ombra #{value}", + "_tab_label": "Ombra i il·luminació", + "inset": "Ombra interior" + }, + "switcher": { + "use_snapshot": "Versió antiga", + "help": { + "future_version_imported": "El fitxer importat es va crear per a una versió del front-end més recent.", + "migration_snapshot_ok": "Per a estar segurs, s'ha carregat la instantà nia del tema. Pots intentar carregar les dades del tema.", + "migration_napshot_gone": "Per alguna raó, faltava la instantà nia, algunes coses podrien veure's diferents del que recordes.", + "snapshot_source_mismatch": "Conflicte de versions: probablement el front-end s'ha revertit i actualitzat una altra volta, si has canviat el tema en una versió anterior, segurament vols utilitzar la versió antiga; d'altra banda utilitza la nova versió.", + "v2_imported": "El fitxer que has importat va ser creat per a un front-end més antic. Intentem maximitzar la compatibilitat, però podrien haver inconsistències.", + "fe_upgraded": "El motor de temes de PleromaFE es va actualitzar després de l'actualització de la versió.", + "snapshot_missing": "No hi havia cap instantà nia del tema al fitxer, per tant podria veure's diferent del previst originalment.", + "upgraded_from_v2": "PleromaFE s'ha actualitzat, el tema pot veure's un poc diferent de com recordes.", + "fe_downgraded": "Versió de PleromaFE revertida.", + "older_version_imported": "El fitxer que has importat va ser creat en una versió del front-end més antiga.", + "snapshot_present": "S'ha carregat la instantà nia del tema, de manera que tots els valors estan sobreescrits. En canvi, podeu carregar les dades reals del tema." + }, + "keep_as_is": "Mantindre com està ", + "save_load_hint": "Les opcions \"Mantindre\" conserven les opcions configurades actualment al seleccionar o carregar temes, també emmagatzema aquestes opcions quan s'exporta un tema. Quan es desactiven totes les caselles de verificació, el tema exportat ho guardarà tot.", + "keep_color": "Mantindre colors", + "keep_opacity": "Mantindre opacitat", + "keep_shadows": "Mantindre ombres", + "keep_fonts": "Mantindre fonts", + "keep_roundness": "Mantindre rodoneses", + "clear_all": "Netejar tot", + "reset": "Reinciar", + "load_theme": "Carregar tema", + "use_source": "Nova versió", + "clear_opacity": "Netejar opacitat" + }, + "common": { + "contrast": { + "hint": "El rà tio de contrast és {ratio}. {level} {context}", + "level": { + "bad": "no compleix amb cap pauta d'accecibilitat", + "aaa": "Compleix amb el nivell AA (recomanat)", + "aa": "Compleix amb el nivell AA (mÃnim)" + }, + "context": { + "18pt": "per a textos grans (+18pt)", + "text": "per a textos" + } + }, + "opacity": "Opacitat", + "color": "Color" + }, + "advanced_colors": { + "badge": "Fons de insÃgnies", + "inputs": "Camps d'entrada", + "wallpaper": "Fons de pantalla", + "pressed": "Pressionat", + "chat": { + "outgoing": "Eixint", + "border": "Borde", + "incoming": "Entrants" + }, + "borders": "Bordes", + "panel_header": "Capçalera del panell", + "buttons": "Botons", + "faint_text": "Text esvaït", + "poll": "Grà fica de l'enquesta", + "toggled": "Commutat", + "alert": "Fons d'alertes", + "alert_error": "Error", + "alert_warning": "Precaució", + "post": "Publicacions/Biografies d'usuaris", + "badge_notification": "Notificacions", + "selectedMenu": "Element del menú seleccionat", + "tabs": "Pestanyes", + "_tab_label": "Avançat", + "alert_neutral": "Neutral", + "popover": "Suggeriments, menús, superposicions", + "top_bar": "Barra superior", + "highlight": "Elements destacats", + "disabled": "Deshabilitat", + "icons": "Icones", + "selectedPost": "Publicació seleccionada", + "underlay": "Subratllat" + }, + "common_colors": { + "main": "Colors comuns", + "rgbo": "Icones, accents, insÃgnies", + "foreground_hint": "mira la pestanya \"Avançat\" per a un control més detallat", + "_tab_label": "Comú" + }, + "radii": { + "_tab_label": "Rodonesa" + } + }, + "version": { + "frontend_version": "Versió \"Frontend\"", + "backend_version": "Versió \"backend\"", + "title": "Versió" + }, + "theme_help_v2_1": "També pots anular alguns components de color i opacitat activant la casella. Usa el botó \"Esborrar tot\" per esborrar totes les anulacions.", + "type_domains_to_mute": "Buscar dominis per a silenciar", + "greentext": "Text verd (meme arrows)", + "fun": "Divertit", + "notification_setting_filters": "Filtres", + "virtual_scrolling": "Optimitzar la representació del flux", + "notification_setting_block_from_strangers": "Bloqueja les notificacions dels usuaris que no segueixes", + "enable_web_push_notifications": "Habilitar notificacions del navegador", + "notification_blocks": "Bloquejar a un usuari para totes les notificacions i també les cancel·la.", + "more_settings": "Més opcions", + "notification_setting_privacy": "Privacitat", + "upload_a_photo": "Pujar una foto", + "notification_setting_hide_notification_contents": "Amagar el remitent i els continguts de les notificacions push", + "notifications": "Notificacions", + "notification_mutes": "Per a deixar de rebre notificacions d'un usuari en concret, silencia'l-ho.", + "theme_help_v2_2": "Les icones per baix d'algunes entrades són indicadors del contrast del fons/text, desplaça el ratolà per a més informació. Tingues en compte que quan s'utilitzen indicadors de contrast de transparència es mostra el pitjor cas possible.", + "hide_shoutbox": "Oculta la casella de gà bia de grills", + "always_show_post_button": "Mostra sempre el botó flotant de publicació nova", + "pad_emoji": "Acompanya els emojis amb espais en afegir des del selector", + "mentions_new_style": "Enllaços d'esment més elegants", + "mentions_new_place": "Posa les mencions en una lÃnia separada", + "post_status_content_type": "Format de publicació" }, "time": { "day": "{0} dia", "days": "{0} dies", "day_short": "{0} dia", "days_short": "{0} dies", - "hour": "{0} hour", - "hours": "{0} hours", + "hour": "{0} hora", + "hours": "{0} hores", "hour_short": "{0}h", "hours_short": "{0}h", "in_future": "in {0}", @@ -287,12 +568,12 @@ "months_short": "{0} mesos", "now": "ara mateix", "now_short": "ara mateix", - "second": "{0} second", - "seconds": "{0} seconds", + "second": "{0} segon", + "seconds": "{0} segons", "second_short": "{0}s", "seconds_short": "{0}s", - "week": "{0} setm.", - "weeks": "{0} setm.", + "week": "{0} setmana", + "weeks": "{0} setmanes", "week_short": "{0} setm.", "weeks_short": "{0} setm.", "year": "{0} any", @@ -308,7 +589,13 @@ "no_retweet_hint": "L'entrada és només per a seguidores o és \"directa\", i per tant no es pot republicar", "repeated": "republicat", "show_new": "Mostra els nous", - "up_to_date": "Actualitzat" + "up_to_date": "Actualitzat", + "socket_reconnected": "Connexió a temps real establerta", + "socket_broke": "Connexió a temps real perduda: codi CloseEvent {0}", + "error": "Error de cà rrega de la lÃnia de temps: {0}", + "no_statuses": "No hi ha entrades", + "reload": "Recarrega", + "no_more_statuses": "No hi ha més entrades" }, "user_card": { "approve": "Aprova", @@ -324,13 +611,62 @@ "muted": "Silenciat", "per_day": "per dia", "remote_follow": "Seguiment remot", - "statuses": "Estats" + "statuses": "Estats", + "unblock_progress": "Desbloquejant…", + "unmute": "Deixa de silenciar", + "follow_progress": "Sol·licitant…", + "admin_menu": { + "force_nsfw": "Marca totes les entrades amb \"No segur per a entorns laborals\"", + "strip_media": "Esborra els audiovisuals de les entrades", + "disable_any_subscription": "Deshabilita completament seguir algú", + "quarantine": "Deshabilita la federació a les entrades de les usuà ries", + "moderation": "Moderació", + "delete_user_confirmation": "Està s completament segur/a? Aquesta acció no es pot desfer.", + "revoke_admin": "Revoca l'Admin", + "activate_account": "Activa el compte", + "deactivate_account": "Desactiva el compte", + "revoke_moderator": "Revoca Moderació", + "delete_account": "Esborra el compte", + "disable_remote_subscription": "Deshabilita seguir algú des d'una instà ncia remota", + "delete_user": "Esborra la usuà ria", + "grant_admin": "Concedir permisos d'Administració", + "grant_moderator": "Concedir permisos de Moderació", + "force_unlisted": "Força que les publicacions no estiguin llistades", + "sandbox": "Força que els missatges siguin només seguidors" + }, + "edit_profile": "Edita el perfil", + "hidden": "Amagat", + "follow_sent": "Petició enviada!", + "unmute_progress": "Deixant de silenciar…", + "bot": "Bot", + "mute_progress": "Silenciant…", + "favorites": "Favorits", + "mention": "Menció", + "follow_unfollow": "Deixa de seguir", + "subscribe": "Subscriu-te", + "show_repeats": "Mostra les repeticions", + "report": "Report", + "its_you": "Ets tu!", + "unblock": "Desbloqueja", + "block_progress": "Bloquejant…", + "message": "Missatge", + "unsubscribe": "Anul·la la subscripció", + "hide_repeats": "Amaga les repeticions", + "highlight": { + "disabled": "Sense ressaltat", + "solid": "Fons sòlid", + "striped": "Fons a ratlles", + "side": "Ratlla lateral" + }, + "media": "Media" }, "user_profile": { - "timeline_title": "Flux personal" + "timeline_title": "Flux personal", + "profile_loading_error": "Disculpes, hi ha hagut un error carregant aquest perfil.", + "profile_does_not_exist": "Disculpes, aquest perfil no existeix." }, "who_to_follow": { - "more": "More", + "more": "Més", "who_to_follow": "A qui seguir" }, "selectable_list": { @@ -338,14 +674,25 @@ }, "remote_user_resolver": { "error": "No trobat.", - "searching_for": "Cercant per" + "searching_for": "Cercant per", + "remote_user_resolver": "Resolució d'usuari remot" }, "interactions": { "load_older": "Carrega antigues interaccions", - "favs_repeats": "Repeticions i favorits" + "favs_repeats": "Repeticions i favorits", + "follows": "Nous seguidors", + "moves": "Migració d'usuaris" }, "emoji": { - "stickers": "Adhesius" + "stickers": "Adhesius", + "keep_open": "Mantindre el selector obert", + "custom": "Emojis personalitzats", + "unicode": "Emojis unicode", + "load_all_hint": "Carregat el primer emoji {saneAmount}, carregar tots els emoji pot causar problemes de rendiment.", + "emoji": "Emoji", + "search_emoji": "Buscar un emoji", + "add_emoji": "Inserir un emoji", + "load_all": "Carregant tots els {emojiAmount} emoji" }, "polls": { "expired": "L'enquesta va acabar fa {0}", @@ -357,7 +704,11 @@ "votes": "vots", "option": "Opció", "add_option": "Afegeix opció", - "add_poll": "Afegeix enquesta" + "add_poll": "Afegeix enquesta", + "expiry": "Temps de vida de l'enquesta", + "people_voted_count": "{count} persona ha votat | {count} persones han votat", + "votes_count": "{count} vot | {count} vots", + "not_enough_options": "L'enquesta no té suficients opcions úniques" }, "media_modal": { "next": "Següent", @@ -365,7 +716,8 @@ }, "importer": { "error": "Ha succeït un error mentre s'importava aquest arxiu.", - "success": "Importat amb èxit." + "success": "Importat amb èxit.", + "submit": "Enviar" }, "image_cropper": { "cancel": "Cancel·la", @@ -379,7 +731,9 @@ }, "domain_mute_card": { "mute_progress": "Silenciant…", - "mute": "Silencia" + "mute": "Silencia", + "unmute": "Deixar de silenciar", + "unmute_progress": "Deixant de silenciar…" }, "about": { "staff": "Equip responsable", @@ -391,16 +745,136 @@ "reject": "Rebutja", "accept_desc": "Aquesta instà ncia només accepta missatges de les següents instà ncies:", "accept": "Accepta", - "simple_policies": "PolÃtiques especÃfiques de la instà ncia" + "simple_policies": "PolÃtiques especÃfiques de la instà ncia", + "ftl_removal_desc": "Aquesta instà ncia elimina les següents instà ncies del flux de la xarxa coneguda:", + "ftl_removal": "Eliminació de la lÃnia de temps coneguda", + "media_nsfw_desc": "Aquesta instà ncia obliga el contingut multimèdia a establir-se com a sensible dins de les publicacions en les següents instà ncies:", + "media_removal": "Eliminació de la multimèdia", + "media_removal_desc": "Aquesta instà ncia elimina els suports multimèdia de les publicacions en les següents instà ncies:", + "media_nsfw": "Forçar contingut multimèdia com a sensible" }, "mrf_policies_desc": "Les polÃtiques MRF controlen el comportament federat de la instà ncia. Les següents polÃtiques estan habilitades:", "mrf_policies": "PolÃtiques MRF habilitades", "keyword": { "replace": "Reemplaça", "reject": "Rebutja", - "keyword_policies": "PolÃtiques de paraules clau" + "keyword_policies": "Filtratge per paraules clau", + "is_replaced_by": "→", + "ftl_removal": "Eliminació de la lÃnia de temps federada" }, "federation": "Federació" } + }, + "shoutbox": { + "title": "Gà bia de Grills" + }, + "status": { + "delete": "Esborra l'entrada", + "delete_confirm": "Segur que vols esborrar aquesta entrada?", + "thread_muted_and_words": ", té les paraules:", + "show_full_subject": "Mostra tot el tema", + "show_content": "Mostra el contingut", + "repeats": "Repeticions", + "bookmark": "Marcadors", + "status_unavailable": "Entrada no disponible", + "expand": "Expandeix", + "copy_link": "Copia l'enllaç a l'entrada", + "hide_full_subject": "Amaga tot el tema", + "favorites": "Favorits", + "replies_list": "Contestacions:", + "mute_conversation": "Silencia la conversa", + "thread_muted": "Fil silenciat", + "hide_content": "Amaga el contingut", + "status_deleted": "S'ha esborrat aquesta entrada", + "nsfw": "No segur per a entorns laborals", + "unbookmark": "Desmarca", + "external_source": "Font externa", + "unpin": "Deixa de destacar al perfil", + "pinned": "Destacat", + "reply_to": "Contesta a", + "pin": "Destaca al perfil", + "unmute_conversation": "Deixa de silenciar la conversa", + "mentions": "Mencions", + "you": "(Tu)", + "plus_more": "+{number} més" + }, + "user_reporting": { + "additional_comments": "Comentaris addicionals", + "forward_description": "Aquest compte és d'un altre servidor. Vols enviar una còpia del report allà també?", + "forward_to": "Endavant a {0}", + "generic_error": "Hi ha hagut un error mentre s'estava processant la teva sol·licitud.", + "title": "Reportant {0}", + "add_comment_description": "Aquest report serà enviat a la moderació a la instà ncia. Pots donar una explicació de per què està s reportant aquest compte:", + "submit": "Envia" + }, + "tool_tip": { + "add_reaction": "Afegeix una Reacció", + "accept_follow_request": "Accepta la sol·licitud de seguir", + "repeat": "Repeteix", + "reply": "Respon", + "favorite": "Favorit", + "user_settings": "Configuració d'usuà ria", + "reject_follow_request": "Rebutja la sol·licitud de seguir", + "bookmark": "Marcador", + "media_upload": "Pujar multimèdia" + }, + "search": { + "no_results": "No hi ha resultats", + "people": "Persones", + "hashtags": "Etiquetes", + "people_talking": "{count} persones parlant", + "person_talking": "{count} persones parlant" + }, + "upload": { + "file_size_units": { + "B": "B", + "KiB": "KiB", + "GiB": "GiB", + "TiB": "TiB", + "MiB": "MiB" + }, + "error": { + "base": "La pujada ha fallat.", + "file_too_big": "Fitxer massa gran [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Prova de nou d'aquà una estona", + "message": "La pujada ha fallat: {0}" + } + }, + "errors": { + "storage_unavailable": "Pleroma no ha pogut accedir a l'emmagatzematge del navegador. El teu inici de sessió o configuració no es desaran i et pots trobar algun altre problema. Prova a habilitar les galetes." + }, + "password_reset": { + "password_reset": "Reinicia la contrasenya", + "forgot_password": "Has oblidat la contrasenya?", + "too_many_requests": "Has arribat al lÃmit d'intents. Prova de nou d'aquà una estona.", + "password_reset_required_but_mailer_is_disabled": "Has de reiniciar la teva contrasenya però el reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instà ncia.", + "placeholder": "El teu correu electrònic o nom d'usuà ria", + "instruction": "Introdueix la teva adreça de correu electrònic o nom d'usuà ria. T'enviarem un enllaç per reiniciar la teva contrasenya.", + "return_home": "Torna a la pà gina principal", + "password_reset_required": "Has de reiniciar la teva contrasenya per iniciar la sessió.", + "password_reset_disabled": "El reinici de la contrasenya està deshabilitat. Si us plau, contacta l'administració de la teva instà ncia.", + "check_email": "Comprova que has rebut al correu electrònic un enllaç per reiniciar la teva contrasenya." + }, + "file_type": { + "image": "Imatge", + "file": "Fitxer", + "video": "VÃdeo", + "audio": "Àudio" + }, + "chats": { + "chats": "Xats", + "new": "Nou xat", + "delete_confirm": "Realment vols esborrar aquest missatge?", + "error_sending_message": "Alguna cosa ha fallat quan s'enviava el missatge.", + "more": "Més", + "delete": "Esborra", + "empty_message_error": "No es pot publicar un missatge buit", + "you": "Tu:", + "message_user": "Missatge {nickname}", + "error_loading_chat": "Alguna cosa ha fallat quan es carregava el xat.", + "empty_chat_list_placeholder": "Encara no tens cap xat. Crea un nou xat!" + }, + "display_date": { + "today": "Avui" } } diff --git a/src/i18n/cs.json b/src/i18n/cs.json index d9aed34a046ca4ed73f6695eb1e9c8ae7d19fc73..ca87214e0dba2b3adbf0dd4bc09cdedc54c33641 100644 --- a/src/i18n/cs.json +++ b/src/i18n/cs.json @@ -407,7 +407,6 @@ "follow": "Sledovat", "follow_sent": "Požadavek odeslán!", "follow_progress": "OdeslÃlám požadavek…", - "follow_again": "Odeslat požadavek znovu?", "follow_unfollow": "PÅ™estat sledovat", "followees": "SledovanÃ", "followers": "SledujÃcÃ", diff --git a/src/i18n/de.json b/src/i18n/de.json index 6fe6ab2c078d627b55d7875c4eb132a1f06b7b41..b659959479234b6809573e07dc04916eac8c2854 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -9,7 +9,9 @@ "scope_options": "Reichweitenoptionen", "text_limit": "Zeichenlimit", "title": "Funktionen", - "who_to_follow": "Wem folgen?" + "who_to_follow": "Vorschläge", + "upload_limit": "Maximale Upload Größe", + "pleroma_chat_messages": "Pleroma Chat" }, "finder": { "error_fetching_user": "Fehler beim Suchen des Benutzers", @@ -28,7 +30,19 @@ "disable": "Deaktivieren", "enable": "Aktivieren", "confirm": "Bestätigen", - "verify": "Verifizieren" + "verify": "Verifizieren", + "role": { + "moderator": "Moderator", + "admin": "Admin" + }, + "peek": "Schau rein", + "close": "Schliessen", + "retry": "Versuche es erneut", + "error_retry": "Bitte versuche es erneut", + "loading": "Lade…", + "flash_content": "Klicken, um den Flash-Inhalt mit Ruffle anzuzeigen (Die Funktion ist experimentell und funktioniert daher möglicherweise nicht).", + "flash_security": "Diese Funktion stellt möglicherweise eine Risiko dar, weil Flash-Inhalte weiterhin potentiell gefährlich sind.", + "flash_fail": "Falsh-Inhalt konnte nicht geladen werden, Details werden in der Konsole angezeigt." }, "login": { "login": "Anmelden", @@ -63,7 +77,11 @@ "search": "Suche", "preferences": "Voreinstellungen", "administration": "Administration", - "who_to_follow": "Wem folgen" + "who_to_follow": "Wem folgen", + "chats": "Chats", + "timelines": "Zeitlinie", + "bookmarks": "Lesezeichen", + "home_timeline": "Heim Zeitlinie" }, "notifications": { "broken_favorite": "Unbekannte Nachricht, suche danach…", @@ -76,7 +94,8 @@ "follow_request": "möchte dir folgen", "migrated_to": "migrierte zu", "reacted_with": "reagierte mit {0}", - "no_more_notifications": "Keine Benachrichtigungen mehr" + "no_more_notifications": "Keine Benachrichtigungen mehr", + "error": "Error beim laden von Neuigkeiten" }, "post_status": { "new_status": "Neuen Status veröffentlichen", @@ -105,7 +124,13 @@ "public": "Dieser Beitrag wird für alle sichtbar sein", "private": "Dieser Beitrag wird nur für deine Follower sichtbar sein", "unlisted": "Dieser Beitrag wird weder in der öffentlichen Zeitleiste noch im gesamten bekannten Netzwerk sichtbar sein" - } + }, + "media_description_error": "Medien konnten nicht neu geladen werden, versuche es erneut", + "empty_status_error": "Eine leere Nachricht ohne Anhänge kann nicht gesendet werden", + "preview_empty": "Leer", + "preview": "Vorschau", + "post": "Post", + "media_description": "Medienbeschreibung" }, "registration": { "bio": "Bio", @@ -124,9 +149,12 @@ "password_confirmation_required": "darf nicht leer sein", "password_confirmation_match": "sollte mit dem Passwort identisch sein" }, - "bio_placeholder": "z.B.\nHallo, ich bin Lain.\nIch bin ein Anime Mödchen aus dem vorstädtischen Japan. Du kennst mich vielleicht vom Wired.", + "bio_placeholder": "z.B.\nHallo, ich bin Lain.\nIch bin ein super süßes blushy-crushy Anime Girl aus dem vorstädtischen Japan. Du kennst mich vielleicht von Wired.", "fullname_placeholder": "z.B. Lain Iwakura", - "username_placeholder": "z.B. lain" + "username_placeholder": "z.B. lain", + "register": "Registrierung", + "reason_placeholder": "Diese Instanz bestätigt Registrierungen manuell. \nLass die Admins wissen warum du dich registrieren willst.", + "reason": "Grund zur Anmeldung" }, "settings": { "attachmentRadius": "Anhänge", @@ -136,7 +164,7 @@ "avatarRadius": "Avatare", "background": "Hintergrund", "bio": "Bio", - "btnRadius": "Buttons", + "btnRadius": "Knöpfe", "cBlue": "Blau (Antworten, folgt dir)", "cGreen": "Grün (Retweet)", "cOrange": "Orange (Favorisieren)", @@ -201,7 +229,7 @@ "name_bio": "Name & Bio", "new_password": "Neues Passwort", "notification_visibility": "Benachrichtigungstypen, die angezeigt werden sollen", - "notification_visibility_follows": "Follows", + "notification_visibility_follows": "Folgt", "notification_visibility_likes": "Favoriten", "notification_visibility_mentions": "Erwähnungen", "notification_visibility_repeats": "Wiederholungen", @@ -268,7 +296,24 @@ "save_load_hint": "Die \"Beibehalten\"-Optionen behalten die aktuell eingestellten Optionen beim Auswählen oder Laden von Designs bei, sie speichern diese Optionen auch beim Exportieren eines Designs. Wenn alle Kontrollkästchen deaktiviert sind, wird beim Exportieren des Designs alles gespeichert.", "reset": "Zurücksetzen", "clear_all": "Alles leeren", - "clear_opacity": "Deckkraft leeren" + "clear_opacity": "Deckkraft leeren", + "help": { + "fe_downgraded": "PleromaFE Version wurde zurückgerollt.", + "older_version_imported": "Die Datei, die du importiert hast, wurde für eine ältere Version vom FE gemacht.", + "future_version_imported": "Die Datei, die du importiert hast, wurde für eine neuere Version vom FE gemacht.", + "v2_imported": "Die Datei, die du importiert hast, war für eine ältere Version des FEs. Wir versuchen, die Kompatibilität zu maximieren, aber es könnte trotzdem Inkonsistenz auftreten.", + "upgraded_from_v2": "PleromaFE wurde modernisiert, dein Theme könnte etwas anders aussehen als vorher.", + "snapshot_source_mismatch": "Versionskonflikt: vermutlich wurde das FE zurückgesetzt und dann ein Update durchgeführt. Falls das Theme mit einer alten FE-Version erstellt wurde, sollte vermutlich die alte Version verwendet werden, andernfalls die neue.", + "migration_napshot_gone": "Snapshot konnte nicht gefunden werden, die Anzeige könnte daher teilweise möglicherweise nicht den Erwartungen entsprechen.", + "migration_snapshot_ok": "Vorsichtshalber wurde ein Snapshot des Themes geladen. Alternativ kann versucht werden, die Daten des Themes selbst zu laden.", + "snapshot_present": "Snapshot des Themes wurde geladen, alle entsprechenden Einstellungen wurden überschrieben. Alternativ können die tatsächlichen Daten des Themes geladen werden.", + "fe_upgraded": "Mit dem Upgrade wurde auch eine neue Version von Pleromas Theme Engine installiert.", + "snapshot_missing": "Die Datei enthält keinen Theme-Snapshot, die Darstellung kann daher möglicherweise abweichend sein." + }, + "use_source": "Neue Version", + "use_snapshot": "Alte Version", + "keep_as_is": "Lass es so, wie es ist", + "load_theme": "Lade Theme" }, "common": { "color": "Farbe", @@ -303,7 +348,27 @@ "borders": "Rahmen", "buttons": "Schaltflächen", "inputs": "Eingabefelder", - "faint_text": "Verblasster Text" + "faint_text": "Verblasster Text", + "disabled": "aus", + "selectedMenu": "Ausgewähltes Menüelement", + "selectedPost": "Ausgewählter Post", + "pressed": "Gedrückt", + "highlight": "Hervorgehobene Elemente", + "icons": "Icons", + "poll": "Umfragegraph", + "post": "Posts/Benutzerinfo", + "alert_neutral": "Neutral", + "alert_warning": "Warnung", + "wallpaper": "Hintergrund", + "popover": "Kurzinfo, Menüs, Popover-Fenster", + "chat": { + "border": "Ränder", + "outgoing": "Ausgehend", + "incoming": "Eingehend" + }, + "toggled": "Umgeschaltet", + "underlay": "Halbtransparenter Hintergrund", + "tabs": "Reiter" }, "radii": { "_tab_label": "Abrundungen" @@ -325,7 +390,7 @@ "inset_classic": "Eingesetzte Schatten werden mit {0} verwendet" }, "components": { - "panel": "Panel", + "panel": "Bedienfeld", "panelHeader": "Panel-Kopf", "topBar": "Obere Leiste", "avatar": "Benutzer-Avatar (in der Profilansicht)", @@ -335,8 +400,9 @@ "buttonHover": "Schaltfläche (hover)", "buttonPressed": "Schaltfläche (gedrückt)", "buttonPressedHover": "Schaltfläche (gedrückt+hover)", - "input": "Input field" - } + "input": "Eingabefeld" + }, + "hintV3": "Um die Farbe der Schatten zu bestimmen, kann auch die Auszeichnung {0} verwendet werden, um einen anderen Fabbereich zu nutzen." }, "fonts": { "_tab_label": "Schriften", @@ -384,11 +450,14 @@ }, "verify": { "desc": "Um 2FA zu aktivieren, gib den Code von deiner 2FA-App ein:" - } + }, + "confirm_and_enable": "Bestätige und aktiviere OTP", + "setup_otp": "Richte OTP ein", + "wait_pre_setup_otp": "OTP voreinstellen" }, "enter_current_password_to_confirm": "Gib dein aktuelles Passwort ein, um deine Identität zu bestätigen", "security": "Sicherheit", - "allow_following_move": "Erlaube automatisches Folgen, sobald ein gefolgter Nutzer umzieht", + "allow_following_move": "Erlaube auto-follow, wenn von dir verfolgte Accounts umziehen", "blocks_imported": "Blocks importiert! Die Verarbeitung wird einen Moment brauchen.", "block_import_error": "Fehler beim Importieren der Blocks", "block_import": "Block Import", @@ -400,7 +469,81 @@ "change_email_error": "Es trat ein Problem auf beim Versuch, deine Email Adresse zu ändern.", "change_email": "Ändere Email", "import_blocks_from_a_csv_file": "Importiere Blocks von einer CSV Datei", - "accent": "Akzent" + "accent": "Akzent", + "no_blocks": "Keine Blocks", + "notification_visibility_emoji_reactions": "Reaktionen", + "new_email": "Neue Email", + "profile_fields": { + "value": "Inhalt", + "name": "Label", + "add_field": "Feld hinzufügen", + "label": "Profil Metadaten" + }, + "bot": "Dies ist ein Bot Account", + "blocks_tab": "Blocks", + "save": "Änderungen speichern", + "show_moderator_badge": "Zeige Moderator-Abzeichen auf meinem Profil", + "show_admin_badge": "Zeige Admin-Abzeichen auf meinem Profil", + "no_mutes": "Keine Stummschaltungen", + "reset_profile_background": "Profilhintergrund zurücksetzen", + "reset_avatar": "Avatar zurücksetzten", + "search_user_to_mute": "Suche, wen du stummschalten willst", + "search_user_to_block": "Suche, wen du blocken willst", + "reply_visibility_self_short": "Zeige antworten nur einem selbst", + "reply_visibility_following_short": "Zeige Antworten an meine Follower", + "notification_visibility_moves": "Nutzer zieht um", + "file_export_import": { + "errors": { + "file_too_new": "Inkompatible Major Version: {fileMajor}, dieses PleromaFE Version (settings ver {feMajor}) ist zu alt", + "invalid_file": "Die ausgewählte Datei kann nicht zur Wiederherstellung verwendet werden. Keine Änderungen wurden umgesetzt.", + "file_too_old": "Inkompatible Major Version: {fileMajor}, die Dateiversion ist zu alt und wird nicht mehr unterstützt (min. set. ver. {feMajor})", + "file_slightly_new": "Geringfügige Abweichung in der Dateiversion, einige Einstellungen konnten möglicherweise nicht geladen werden" + }, + "restore_settings": "Einstellungen von einer Datei wiederherstellen", + "backup_settings_theme": "Einstellungen und Theme in eine Datei speichern", + "backup_settings": "Einstellungen in Datei speichern", + "backup_restore": "Einstellungen backuppen" + }, + "hide_wallpaper": "Verstecke Instanzhintergrundbild", + "hide_all_muted_posts": "Verstecke stummgeschaltete Posts", + "hide_media_previews": "Verstecke Vorschau von Medien", + "word_filter": "Wort Filter", + "mutes_and_blocks": "Stummgeschaltete und Geblockte", + "chatMessageRadius": "Chat Nachricht", + "import_mutes_from_a_csv_file": "Importiere stummgeschaltete User von einer cvs Datei", + "mutes_imported": "Stummgeschaltete User wurden importiert! Verarbeitung dauert eine Weile.", + "mute_import_error": "Fehler beim Importieren von stummgeschalteten Usern", + "mute_import": "Stumm geschaltete User importieren", + "mute_export_button": "Stumm geschaltete User in eine cvs Datei exportieren", + "mute_export": "Stumm geschaltete User exportieren", + "setting_changed": "Einstellungen weichen von den Standardeinstellungen ab", + "notification_blocks": "Einen User zu blocken stoppt alle Benachrichtigungen von ihm und deabonniert ihn.", + "version": { + "frontend_version": "Frontend Version", + "backend_version": "Backend Version", + "title": "Version" + }, + "notification_mutes": "Um nicht mehr die Benachrichtigungen von einem bestimmten User zu bekommen, verwende eine Stummschaltung.", + "user_mutes": "User", + "notification_setting_privacy": "Privatsphäre", + "notification_setting_filters": "Filter", + "greentext": "Meme Pfeile", + "fun": "Spaß", + "upload_a_photo": "Lade ein Foto hoch", + "type_domains_to_mute": "Tippe die Domains ein, die du stummschalten willst", + "useStreamingApiWarning": "(Nicht empfohlen, experimentell, bekannt dafür, Posts zu überspringen)", + "useStreamingApi": "Empfange Posts und Benachrichtigungen in Echtzeit", + "more_settings": "Weitere Einstellungen", + "notification_setting_hide_notification_contents": "Absender und Inhalte von Push-Nachrichten verbergen", + "notification_setting_block_from_strangers": "Benachrichtigungen von Nutzern blockieren, denen Du nicht folgst", + "virtual_scrolling": "Rendering der Timeline optimieren", + "sensitive_by_default": "Alle Beiträge standardmäßig als heikel markieren", + "reset_background_confirm": "Hintergrund wirklich zurücksetzen?", + "reset_banner_confirm": "Banner wirklich zurücksetzen?", + "reset_avatar_confirm": "Avatar wirklich zurücksetzen?", + "reset_profile_banner": "Profilbanner zurücksetzen", + "hide_shoutbox": "Shoutbox der Instanz verbergen", + "right_sidebar": "Seitenleiste rechts anzeigen" }, "timeline": { "collapse": "Einklappen", @@ -410,7 +553,13 @@ "no_retweet_hint": "Der Beitrag ist als nur-für-Follower oder als Direktnachricht markiert und kann nicht wiederholt werden", "repeated": "wiederholte", "show_new": "Zeige Neuere", - "up_to_date": "Aktuell" + "up_to_date": "Aktuell", + "no_statuses": "Keine Beiträge", + "no_more_statuses": "Keine weiteren Beiträge", + "reload": "Neu laden", + "error": "Fehler beim Lesen der Timeline: {0}", + "socket_broke": "Netzverbindung verloren: CloseEvent code {0}", + "socket_reconnected": "Netzverbindung hergestellt" }, "user_card": { "approve": "Genehmigen", @@ -420,7 +569,6 @@ "follow": "Folgen", "follow_sent": "Anfrage gesendet!", "follow_progress": "Anfragen…", - "follow_again": "Anfrage erneut senden?", "follow_unfollow": "Folgen beenden", "followees": "Folgt", "followers": "Folgende", @@ -433,11 +581,52 @@ "remote_follow": "Folgen", "statuses": "Beiträge", "admin_menu": { - "sandbox": "Erzwinge Beiträge nur für Follower sichtbar zu sein" + "sandbox": "Erzwinge Beiträge nur für Follower sichtbar zu sein", + "delete_user_confirmation": "Achtung! Diese Entscheidung kann nicht rückgängig gemacht werden! Trotzdem durchführen?", + "grant_admin": "Administratorprivilegien gewähren", + "delete_user": "Nutzer löschen", + "strip_media": "Medien von Beiträgen entfernen", + "force_nsfw": "Alle Beiträge als pervers markieren", + "activate_account": "Aktiviere Account", + "revoke_moderator": "Administratorstatuß wiederrufen", + "grant_moderator": "Moderatorstatuß gewähren", + "revoke_admin": "Administratorstatuß wiederrufen", + "moderation": "Moderation", + "delete_account": "Konto löschen", + "deactivate_account": "Konto deaktivieren", + "quarantine": "Beiträge des Nutzers können nur auf der eigenen Instanz gesehen werden", + "disable_any_subscription": "Alle Folgeanfragen für diesen Nutzer grundsätzlich ablehnen", + "disable_remote_subscription": "Nutzer anderer Instanzen vom Folgen dieses Nutzers ausschließen", + "force_unlisted": "Beiträge von der öffentlichen Zeitleiste ausschliessen" + }, + "block_progress": "Blocken…", + "unblock_progress": "Entblocken…", + "unblock": "Entblocken", + "report": "Melden", + "mention": "Erwähnungen", + "media": "Medien", + "hidden": "Versteckt", + "favorites": "Favoriten", + "bot": "Bot", + "show_repeats": "Geteilte Beiträge anzeigen", + "hide_repeats": "Geteilte Beiträge nicht anzeigen", + "mute_progress": "Stummschalten erfolgt…", + "unmute_progress": "Aufhebung erfolgt…", + "unmute": "Stummschalten aufheben", + "unsubscribe": "Entfolgen", + "subscribe": "Folgen", + "message": "Nachricht", + "highlight": { + "side": "Randmarkierung", + "striped": "gestreifter Hintergrund", + "solid": "kein Muster verwenden", + "disabled": "Nicht hervorheben" } }, "user_profile": { - "timeline_title": "Beiträge" + "timeline_title": "Beiträge", + "profile_loading_error": "Beim Laden dieses Profils ist ein Fehler aufgetreten.", + "profile_does_not_exist": "Profil nicht vorhanden." }, "who_to_follow": { "more": "Mehr", @@ -448,13 +637,18 @@ "repeat": "Wiederholen", "reply": "Antworten", "favorite": "Favorisieren", - "user_settings": "Benutzereinstellungen" + "user_settings": "Benutzereinstellungen", + "bookmark": "Lesezeichen", + "reject_follow_request": "Folgeanfrage ablehnen", + "accept_follow_request": "Folgeanfrage annehmen", + "add_reaction": "Emoji-Reaktion hinzufügen" }, "upload": { "error": { "base": "Hochladen fehlgeschlagen.", "file_too_big": "Datei ist zu groß [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Bitte versuche es später erneut" + "default": "Bitte versuche es später erneut", + "message": "Hochladen fehlgeschlagen" }, "file_size_units": { "B": "B", @@ -478,7 +672,7 @@ "placeholder": "Dein Benutzername oder die zugehörige E-Mail-Adresse", "check_email": "Im E-Mail-Posteingang des angebenen Kontos müsste sich jetzt (oder zumindest in Kürze) die E-Mail mit dem Link zum Passwortzurücksetzen befinden.", "return_home": "Zurück zur Heimseite", - "too_many_requests": "Kurze Pause. Zu viele Versuche. Bitte, später nochmal probieren.", + "too_many_requests": "Kurze Pause. Zu viele Versuche. Bitte später nochmal probieren.", "password_reset_disabled": "Passwortzurücksetzen deaktiviert. Bitte Administrator kontaktieren.", "password_reset_required": "Passwortzurücksetzen erforderlich.", "password_reset_required_but_mailer_is_disabled": "Passwortzurücksetzen wäre erforderlich, ist aber deaktiviert. Bitte Administrator kontaktieren." @@ -486,21 +680,21 @@ "about": { "mrf": { "federation": "Föderation", - "mrf_policies": "Aktivierte MRF Richtlinien", + "mrf_policies": "Aktive MRF-Richtlinien", "simple": { "simple_policies": "Instanzspezifische Richtlinien", "accept": "Akzeptieren", "reject": "Ablehnen", "reject_desc": "Diese Instanz akzeptiert keine Nachrichten der folgenden Instanzen:", "quarantine": "Quarantäne", - "ftl_removal": "Von der Zeitleiste \"Das gesamte bekannte Netzwerk\" entfernen", + "ftl_removal": "Von der Zeitleiste \"Das bekannte Netzwerk\" entfernen", "media_removal": "Medienentfernung", "media_removal_desc": "Diese Instanz entfernt Medien von den Beiträgen der folgenden Instanzen:", "media_nsfw": "Erzwingen Medien als heikel zu makieren", "media_nsfw_desc": "Diese Instanz makiert die Medien in Beiträgen der folgenden Instanzen als heikel:", "accept_desc": "Diese Instanz akzeptiert nur Nachrichten von den folgenden Instanzen:", "quarantine_desc": "Diese Instanz sendet nur öffentliche Beiträge zu den folgenden Instanzen:", - "ftl_removal_desc": "Dieser Instanz entfernt folgende Instanzen von der \"Das gesamte bekannte Netzwerk\" Zeitleiste:" + "ftl_removal_desc": "Dieser Instanz entfernt folgende Instanzen von der \"Das bekannte Netzwerk\" Zeitleiste:" }, "keyword": { "keyword_policies": "Keyword Richtlinien", @@ -509,7 +703,7 @@ "is_replaced_by": "→", "ftl_removal": "Von der Zeitleiste \"Das gesamte bekannte Netzwerk\" entfernen" }, - "mrf_policies_desc": "MRF Richtlinien manipulieren das Föderationsverhalten dieser Instanz. Die folgenden Richtlinien sind aktiv:" + "mrf_policies_desc": "MRF Richtlinien beeinflussen das Föderationsverhalten dieser Instanz. Die folgenden Richtlinien sind aktiv:" }, "staff": "Mitarbeiter" }, @@ -550,7 +744,9 @@ "expiry": "Alter der Umfrage", "expired": "Die Umfrage endete vor {0}", "not_enough_options": "Zu wenig einzigartige Auswahlmöglichkeiten in der Umfrage", - "expires_in": "Die Umfrage endet in {0}" + "expires_in": "Die Umfrage endet in {0}", + "votes_count": "{count} Stimme | {count} Stimmen", + "people_voted_count": "{count} Person hat gewählt | {count} Personen haben gewählt" }, "emoji": { "stickers": "Sticker", @@ -560,12 +756,12 @@ "keep_open": "Auswahlfenster offen halten", "add_emoji": "Emoji einfügen", "load_all": "Lade alle {emojiAmount} Emoji", - "load_all_hint": "Erfolgreich erste {saneAmount} Emoji geladen, alle Emojis zu laden würde Leistungsprobleme hervorrufen.", + "load_all_hint": "Erste {saneAmount} Emoji geladen, alle Emoji zu laden könnte Leistungsprobleme verursachen.", "unicode": "Unicode Emoji" }, "interactions": { "load_older": "Lade ältere Interaktionen", - "follows": "Neue Follows", + "follows": "Neue Follower", "favs_repeats": "Wiederholungen und Favoriten", "moves": "Benutzer migriert zu" }, @@ -573,7 +769,106 @@ "select_all": "Wähle alle" }, "remote_user_resolver": { - "searching_for": "Suche nach", - "error": "Nicht gefunden." + "searching_for": "Suche für", + "error": "Nicht gefunden.", + "remote_user_resolver": "Resolver für Nutzer auf anderen Instanzen" + }, + "errors": { + "storage_unavailable": "Pleroma konnte nicht auf den Browser Speicher zugreifen. Deine Anmeldung und deine Einstellungen werden nicht gespeichert. Es kann unvorhersehbare Probleme geben. Versuche ansonsten Cookies zu erlauben." + }, + "shoutbox": { + "title": "Shoutbox" + }, + "chats": { + "error_sending_message": "Beim Senden der Nachricht ist ein Fehler aufgetreten.", + "error_loading_chat": "Beim Laden des Chats ist ein Fehler aufgetreten.", + "delete_confirm": "Soll diese Nachricht wirklich gelöscht werden?", + "empty_message_error": "Die Nachricht darf nicht leer sein", + "delete": "Löschen", + "message_user": "Nachricht an {nickname} senden", + "empty_chat_list_placeholder": "Es sind noch keine Chats vorhanden. Jetzt einen Chat starten!", + "more": "Mehr", + "you": "Du:", + "new": "Neuer Chat", + "chats": "Chats" + }, + "user_reporting": { + "generic_error": "Beim Verarbeiten der Anfrage ist ein Fehler aufgetreten.", + "submit": "Senden", + "forward_to": "Weiterleiten an {0}", + "forward_description": "Das fragliche Konto befindet sich auf einem anderen Server. Soll eine Kopie der Beschwerde an den dortigen Verantwortlichen gesendet werden?", + "additional_comments": "Weitere Anmerkungen", + "add_comment_description": "Die Beschwerde wird an die Moderatoren dieser Instanz gesendet. Die Gründe für die Beschwerde können hier angegeben werden:", + "title": "{0} melddn" + }, + "status": { + "copy_link": "Beitragslink kopieren", + "status_unavailable": "Beitrag nicht verfügbar", + "unmute_conversation": "Konversation nicht mehr stummstellen", + "mute_conversation": "Konversation stummstellen", + "replies_list": "Antworten:", + "reply_to": "Antworten auf", + "delete_confirm": "Möchtest du diese Beitrag wirklich löschen?", + "pinned": "Angeheftet", + "unpin": "Nicht mehr an Profil anheften", + "pin": "An Profil anheften", + "delete": "Lösche Beitrag", + "favorites": "Favoriten", + "expand": "Ausklappen", + "nsfw": "NSFW", + "status_deleted": "Dieser Beitrag wurde gelöscht", + "hide_content": "Inhalt verbergen", + "show_content": "Inhalt anzeigen", + "hide_full_subject": "Vollständiges Thema verbergen", + "show_full_subject": "Vollständiges Thema anzeigen", + "thread_muted": "Thread stummgeschaltet", + "external_source": "Externe Quelle", + "unbookmark": "Lesezeichen entfernen", + "bookmark": "Lesezeichen setzen", + "repeats": "Geteilte Beiträge", + "thread_muted_and_words": ", enthält folgende Wörter:" + }, + "time": { + "seconds_short": "{0}s", + "second_short": "{0}s", + "seconds": "{0} Sekunden", + "second": "{0} Sekunde", + "now_short": "jetzt", + "years_short": "{0}Jhr", + "year_short": "{0}Jhr", + "years": "{0} Jahren", + "year": "{0} Jahr", + "weeks_short": "{0}W", + "week_short": "{0}W", + "weeks": "{0} Wochen", + "week": "{0} Woche", + "now": "gerade eben", + "months_short": "{0}Mo", + "month_short": "{0}Mo", + "months": "{0} Monaten", + "month": "{0} Monat", + "minutes_short": "{0}Min", + "minute_short": "{0}Min", + "minutes": "{0} Minuten", + "minute": "{0} Minute", + "in_past": "vor {0}", + "in_future": "in {0}", + "hours_short": "{0}Std", + "hour_short": "{0}Std", + "hours": "{0} Stunden", + "hour": "{0} Stunde", + "days_short": "{0}T", + "day_short": "{0}T", + "days": "{0} Tage", + "day": "{0} Tag" + }, + "display_date": { + "today": "Heute" + }, + "file_type": { + "file": "Datei", + "image": "Bild", + "video": "Video", + "audio": "Audio" } } diff --git a/src/i18n/en.json b/src/i18n/en.json index 9e74840fc314ecb48e966e1723ea800143bc7839..90a9c19f61320760c1ce036c6e042ef102601475 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -3,27 +3,30 @@ "mrf": { "federation": "Federation", "keyword": { - "keyword_policies": "Keyword Policies", + "keyword_policies": "Keyword policies", "ftl_removal": "Removal from \"The Whole Known Network\" Timeline", "reject": "Reject", "replace": "Replace", "is_replaced_by": "→" }, - "mrf_policies": "Enabled MRF Policies", + "mrf_policies": "Enabled MRF policies", "mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:", "simple": { - "simple_policies": "Instance-specific Policies", + "simple_policies": "Instance-specific policies", + "instance": "Instance", + "reason": "Reason", + "not_applicable": "N/A", "accept": "Accept", "accept_desc": "This instance only accepts messages from the following instances:", "reject": "Reject", "reject_desc": "This instance will not accept messages from the following instances:", "quarantine": "Quarantine", "quarantine_desc": "This instance will send only public posts to the following instances:", - "ftl_removal": "Removal from \"The Whole Known Network\" Timeline", - "ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:", + "ftl_removal": "Removal from \"Known Network\" Timeline", + "ftl_removal_desc": "This instance removes these instances from \"Known Network\" timeline:", "media_removal": "Media Removal", "media_removal_desc": "This instance removes media from posts on the following instances:", - "media_nsfw": "Media Force-set As Sensitive", + "media_nsfw": "Media force-set as sensitive", "media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:" } }, @@ -76,7 +79,14 @@ "confirm": "Confirm", "verify": "Verify", "close": "Close", - "peek": "Peek" + "peek": "Peek", + "role": { + "admin": "Admin", + "moderator": "Moderator" + }, + "flash_content": "Click to show Flash content using Ruffle (Experimental, may not work).", + "flash_security": "Note that this can be potentially dangerous since Flash content is still arbitrary code.", + "flash_fail": "Failed to load flash content, see console for details." }, "image_cropper": { "crop_picture": "Crop picture", @@ -109,18 +119,20 @@ }, "media_modal": { "previous": "Previous", - "next": "Next" + "next": "Next", + "counter": "{current} / {total}" }, "nav": { "about": "About", "administration": "Administration", "back": "Back", - "friend_requests": "Follow Requests", + "friend_requests": "Follow requests", "mentions": "Mentions", "interactions": "Interactions", - "dms": "Direct Messages", - "public_tl": "Public Timeline", + "dms": "Direct messages", + "public_tl": "Public timeline", "timeline": "Timeline", + "home_timeline": "Home timeline", "twkn": "Known Network", "bookmarks": "Bookmarks", "user_search": "User Search", @@ -146,10 +158,12 @@ "submitted_report": "submitted a report" }, "polls": { - "add_poll": "Add Poll", - "add_option": "Add Option", + "add_poll": "Add poll", + "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", @@ -174,7 +188,7 @@ "storage_unavailable": "Pleroma could not access browser storage. Your login or your local settings won't be saved and you might encounter unexpected issues. Try enabling cookies." }, "interactions": { - "favs_repeats": "Repeats and Favorites", + "favs_repeats": "Repeats and favorites", "follows": "New follows", "emoji_reactions": "Emoji Reactions", "reports": "Reports", @@ -198,6 +212,7 @@ "direct_warning_to_all": "This post will be visible to all the mentioned users.", "direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.", "posting": "Posting", + "post": "Post", "preview": "Preview", "preview_empty": "Empty", "empty_status_error": "Can't post an empty status with no files", @@ -208,10 +223,10 @@ "unlisted": "This post will not be visible in Public Timeline and The Whole Known Network" }, "scope": { - "direct": "Direct - Post to mentioned users only", - "private": "Followers-only - Post to followers only", - "public": "Public - Post to public timelines", - "unlisted": "Unlisted - Do not post to public timelines" + "direct": "Direct - post to mentioned users only", + "private": "Followers-only - post to followers only", + "public": "Public - post to public timelines", + "unlisted": "Unlisted - do not post to public timelines" } }, "registration": { @@ -226,6 +241,9 @@ "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.", + "register": "Register", "validations": { "username_required": "cannot be left blank", "fullname_required": "cannot be left blank", @@ -255,8 +273,11 @@ }, "settings": { "app_name": "App name", + "save": "Save changes", "security": "Security", + "setting_changed": "Setting is different from default", "enter_current_password_to_confirm": "Enter your current password to confirm your identity", + "post_look_feel": "Posts Look & Feel", "mfa": { "otp": "OTP", "setup_otp": "Setup OTP", @@ -282,7 +303,7 @@ "attachmentRadius": "Attachments", "attachments": "Attachments", "avatar": "Avatar", - "avatarAltRadius": "Avatars (Notifications)", + "avatarAltRadius": "Avatars (notifications)", "avatarRadius": "Avatars", "background": "Background", "bio": "Bio", @@ -304,10 +325,10 @@ "cGreen": "Green (Retweet)", "cOrange": "Orange (Favorite)", "cRed": "Red (Cancel)", - "change_email": "Change Email", + "change_email": "Change email", "change_email_error": "There was an issue changing your email.", "changed_email": "Email changed successfully!", - "change_password": "Change Password", + "change_password": "Change password", "change_password_error": "There was an issue changing your password.", "changed_password": "Password changed successfully!", "chatMessageRadius": "Chat message", @@ -316,9 +337,9 @@ "confirm_new_password": "Confirm new password", "current_password": "Current password", "mutes_and_blocks": "Mutes and Blocks", - "data_import_export_tab": "Data Import / Export", + "data_import_export_tab": "Data import / export", "default_vis": "Default visibility scope", - "delete_account": "Delete Account", + "delete_account": "Delete account", "delete_account_description": "Permanently delete your data and deactivate your account.", "delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.", "delete_account_instructions": "Type your password in the input below to confirm account deletion.", @@ -329,7 +350,9 @@ "emoji_reactions_on_timeline": "Show emoji reactions on timeline", "export_theme": "Save preset", "filtering": "Filtering", + "wordfilter": "Wordfilter", "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", @@ -340,15 +363,22 @@ "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", - "max_thumbnails": "Maximum amount of thumbnails per post", + "hide_all_muted_posts": "Hide muted posts", + "max_thumbnails": "Maximum amount of thumbnails per post (empty = no limit)", "hide_isp": "Hide instance-specific panel", + "hide_shoutbox": "Hide instance shoutbox", + "right_sidebar": "Show sidebar on the right side", + "always_show_post_button": "Always show floating New Post button", "hide_wallpaper": "Hide instance wallpaper", "preload_images": "Preload images", "use_one_click_nsfw": "Open NSFW attachments with just one click", "hide_post_stats": "Hide post statistics (e.g. the number of favorites)", "hide_user_stats": "Hide user statistics (e.g. the number of followers)", - "hide_filtered_statuses": "Hide filtered statuses", + "hide_filtered_statuses": "Hide all filtered posts", + "hide_wordfiltered_statuses": "Hide word-filtered statuses", + "hide_muted_threads": "Hide muted threads", "import_blocks_from_a_csv_file": "Import blocks from a csv file", "import_followers_from_a_csv_file": "Import follows from a csv file", "import_theme": "Load preset", @@ -366,20 +396,34 @@ "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", "mutes_tab": "Mutes", "play_videos_in_modal": "Play videos in a popup frame", + "file_export_import": { + "backup_restore": "Settings backup", + "backup_settings": "Backup settings to file", + "backup_settings_theme": "Backup settings and theme to file", + "restore_settings": "Restore settings from file", + "errors": { + "invalid_file": "The selected file is not a supported Pleroma settings backup. No changes were made.", + "file_too_new": "Incompatile major version: {fileMajor}, this PleromaFE (settings ver {feMajor}) is too old to handle it", + "file_too_old": "Incompatile major version: {fileMajor}, file version is too old and not supported (min. set. ver. {feMajor})", + "file_slightly_new": "File minor version is different, some settings might not load" + } + }, "profile_fields": { "label": "Profile metadata", - "add_field": "Add Field", + "add_field": "Add field", "name": "Label", "value": "Content" }, "use_contain_fit": "Don't crop the attachment in thumbnails", "name": "Name", - "name_bio": "Name & Bio", - "new_email": "New Email", + "name_bio": "Name & bio", + "new_email": "New email", "new_password": "New password", + "posts": "Posts", + "user_profiles": "User Profiles", "notification_visibility": "Types of notifications to show", "notification_visibility_follows": "Follows", - "notification_visibility_likes": "Likes", + "notification_visibility_likes": "Favorites", "notification_visibility_mentions": "Mentions", "notification_visibility_repeats": "Repeats", "notification_visibility_moves": "User Migrates", @@ -391,25 +435,27 @@ "hide_followers_description": "Don't show who's following me", "hide_follows_count_description": "Don't show follow count", "hide_followers_count_description": "Don't show follower count", - "show_admin_badge": "Show Admin badge in my profile", - "show_moderator_badge": "Show Moderator badge in my profile", - "nsfw_clickthrough": "Enable clickthrough attachment and link preview image hiding for NSFW statuses", + "show_admin_badge": "Show \"Admin\" badge in my profile", + "show_moderator_badge": "Show \"Moderator\" badge in my profile", + "nsfw_clickthrough": "Hide sensitive/NSFW media", "oauth_tokens": "OAuth tokens", "token": "Token", - "refresh_token": "Refresh Token", - "valid_until": "Valid Until", + "refresh_token": "Refresh token", + "valid_until": "Valid until", "revoke_token": "Revoke", "panelRadius": "Panels", "pause_on_unfocused": "Pause streaming when tab is not focused", "presets": "Presets", - "profile_background": "Profile Background", - "profile_banner": "Profile Banner", + "profile_background": "Profile background", + "profile_banner": "Profile banner", "profile_tab": "Profile", "radii_help": "Set up interface edge rounding (in pixels)", "replies_in_timeline": "Replies in timeline", "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", @@ -434,7 +480,8 @@ "subject_line_mastodon": "Like mastodon: copy as is", "subject_line_noop": "Do not copy", "post_status_content_type": "Post status content type", - "stop_gifs": "Play-on-hover GIFs", + "sensitive_by_default": "Mark posts as sensitive by default", + "stop_gifs": "Pause animated images until you hover on them", "streaming": "Enable automatic streaming of new posts when scrolled to the top", "user_mutes": "Users", "useStreamingApi": "Receive posts and notifications real-time", @@ -453,8 +500,18 @@ "true": "yes" }, "virtual_scrolling": "Optimize timeline rendering", + "use_at_icon": "Display @ symbol as an icon instead of text", + "mention_link_display": "Display mention links", + "mention_link_display_short": "always as short names (e.g. @foo)", + "mention_link_display_full_for_remote": "as full names only for remote users (e.g. @foo@example.org)", + "mention_link_display_full": "always as full names (e.g. @foo@example.org)", + "mention_link_show_tooltip": "Show full user names as tooltip for remote users", + "mention_link_show_avatar": "Show user avatar beside the link", + "mention_link_fade_domain": "Fade domains (e.g. @example.org in @foo@example.org)", + "mention_link_bolden_you": "Highlight mention of you when you are mentioned", "fun": "Fun", "greentext": "Meme arrows", + "show_yous": "Show (You)s", "notifications": "Notifications", "notification_setting_filters": "Filters", "notification_setting_block_from_strangers": "Block notifications from users who you do not follow", @@ -463,6 +520,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", @@ -611,8 +669,8 @@ }, "version": { "title": "Version", - "backend_version": "Backend Version", - "frontend_version": "Frontend Version" + "backend_version": "Backend version", + "frontend_version": "Frontend version" } }, "time": { @@ -660,7 +718,9 @@ "reload": "Reload", "up_to_date": "Up-to-date", "no_more_statuses": "No more statuses", - "no_statuses": "No statuses" + "no_statuses": "No statuses", + "socket_reconnected": "Realtime connection established", + "socket_broke": "Realtime connection lost: CloseEvent code {0}" }, "status": { "favorites": "Favorites", @@ -673,6 +733,7 @@ "unbookmark": "Unbookmark", "delete_confirm": "Do you really want to delete this status?", "reply_to": "Reply to", + "mentions": "Mentions", "replies_list": "Replies:", "mute_conversation": "Mute conversation", "unmute_conversation": "Unmute conversation", @@ -687,18 +748,33 @@ "hide_content": "Hide content", "status_deleted": "This post was deleted", "nsfw": "NSFW", - "expand": "Expand" + "expand": "Expand", + "you": "(You)", + "plus_more": "+{number} more", + "many_attachments": "Post has {number} attachment(s)", + "collapse_attachments": "Collapse attachments", + "show_all_attachments": "Show all attachments", + "show_attachment_in_modal": "Show in media modal", + "show_attachment_description": "Preview description (open attachment for full description)", + "hide_attachment": "Hide attachment", + "remove_attachment": "Remove attachment", + "attachment_stop_flash": "Stop Flash player", + "move_up": "Shift attachment left", + "move_down": "Shift attachment right", + "open_gallery": "Open gallery" }, "user_card": { "approve": "Approve", "block": "Block", "blocked": "Blocked!", + "deactivated": "Deactivated", "deny": "Deny", + "edit_profile": "Edit profile", "favorites": "Favorites", "follow": "Follow", + "follow_cancel": "Cancel request", "follow_sent": "Request sent!", "follow_progress": "Requesting…", - "follow_again": "Send request again?", "follow_unfollow": "Unfollow", "followees": "Following", "followers": "Followers", @@ -725,6 +801,7 @@ "mute_progress": "Muting…", "hide_repeats": "Hide repeats", "show_repeats": "Show repeats", + "bot": "Bot", "admin_menu": { "moderation": "Moderation", "grant_admin": "Grant Admin", @@ -744,13 +821,15 @@ "delete_user": "Delete user", "delete_user_confirmation": "Are you absolutely sure? This action cannot be undone." }, - "roles": { - "admin": "Admin", - "moderator": "Moderator" + "highlight": { + "disabled": "No highlight", + "solid": "Solid bg", + "striped": "Striped bg", + "side": "Side stripe" } }, "user_profile": { - "timeline_title": "User Timeline", + "timeline_title": "User timeline", "profile_does_not_exist": "Sorry, this profile does not exist.", "profile_loading_error": "Sorry, there was an error loading this profile." }, @@ -768,7 +847,7 @@ "who_to_follow": "Who to follow" }, "tool_tip": { - "media_upload": "Upload Media", + "media_upload": "Upload media", "repeat": "Repeat", "reply": "Reply", "favorite": "Favorite", diff --git a/src/i18n/eo.json b/src/i18n/eo.json index 58f8e1253346160bddb0f84e3c1427a50159a6f7..659b59607ee7970a19084ccdaf0f3f2229a0a546 100644 --- a/src/i18n/eo.json +++ b/src/i18n/eo.json @@ -35,7 +35,14 @@ "retry": "Reprovi", "error_retry": "Bonvolu reprovi", "loading": "Enlegante…", - "peek": "AntaÅmontri" + "peek": "AntaÅmontri", + "role": { + "moderator": "Reguligisto", + "admin": "Administranto" + }, + "flash_content": "Klaku por montri enhavon de Flash per Ruffle. (Eksperimente, eble ne funkcios.)", + "flash_security": "Sciu, ke tio povas esti danÄera, ĉar la enhavo de Flash ja estas arbitra programo.", + "flash_fail": "Malsukcesis enlegi enhavon de Flash; vidu detalojn en konzolo." }, "image_cropper": { "crop_picture": "Tondi bildon", @@ -83,7 +90,8 @@ "interactions": "Interagoj", "administration": "Administrado", "bookmarks": "Legosignoj", - "timelines": "Historioj" + "timelines": "Historioj", + "home_timeline": "Hejma historio" }, "notifications": { "broken_favorite": "Nekonata stato, serĉante Äin…", @@ -115,10 +123,10 @@ "direct_warning": "Ĉi tiu afiÅo estos videbla nur por ĉiuj menciitaj uzantoj.", "posting": "AfiÅante", "scope": { - "direct": "Rekta – AfiÅi nur al menciitaj uzantoj", - "private": "Nur abonantoj – AfiÅi nur al abonantoj", - "public": "Publika – AfiÅi al publikaj historioj", - "unlisted": "Nelistigita – Ne afiÅi al publikaj historioj" + "direct": "Rekta – afiÅi nur al menciitaj uzantoj", + "private": "Nur abonantoj – afiÅi nur al abonantoj", + "public": "Publika – afiÅi al publikaj historioj", + "unlisted": "Nelistigita – ne afiÅi al publikaj historioj" }, "scope_notice": { "unlisted": "Ĉi tiu afiÅo ne estos videbla en la Publika historio kaj La tuta konata reto", @@ -131,7 +139,8 @@ "preview": "AntaÅrigardo", "direct_warning_to_first_only": "Ĉi tiu afiÅo estas nur videbla al uzantoj menciitaj je la komenco de la mesaÄo.", "direct_warning_to_all": "Ĉi tiu afiÅo estos videbla al ĉiuj menciitaj uzantoj.", - "media_description": "Priskribo de vidaÅdaĵo" + "media_description": "Priskribo de vidaÅdaĵo", + "post": "AfiÅo" }, "registration": { "bio": "Priskribo", @@ -139,7 +148,7 @@ "fullname": "Prezenta nomo", "password_confirm": "Konfirmo de pasvorto", "registration": "RegistriÄo", - "token": "Invita ĵetono", + "token": "Invita peco", "captcha": "TESTO DE HOMECO", "new_captcha": "Klaku la bildon por akiri novan teston", "username_placeholder": "ekz. lain", @@ -152,7 +161,10 @@ "password_required": "ne povas resti malplena", "password_confirmation_required": "ne povas resti malplena", "password_confirmation_match": "samu la pasvorton" - } + }, + "reason_placeholder": "Ĉi-node oni aprobas registriÄojn permane.\nSciigu la administrantojn kial vi volas registriÄi.", + "reason": "Kialo registriÄi", + "register": "RegistriÄi" }, "settings": { "app_name": "Nomo de aplikaĵo", @@ -238,9 +250,9 @@ "show_admin_badge": "Montri la insignon de administranto en mia profilo", "show_moderator_badge": "Montri la insignon de reguligisto en mia profilo", "nsfw_clickthrough": "Åœalti traklakan kaÅadon de kunsendaĵoj kaj antaÅmontroj de ligiloj por konsternaj statoj", - "oauth_tokens": "Ä´etonoj de OAuth", - "token": "Ä´etono", - "refresh_token": "Ä´etono de aktualigo", + "oauth_tokens": "Pecoj de OAuth", + "token": "Peco", + "refresh_token": "Aktualiga peco", "valid_until": "Valida Äis", "revoke_token": "Senvalidigi", "panelRadius": "Bretoj", @@ -365,7 +377,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 +529,34 @@ "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", + "more_settings": "Pliaj agordoj", + "sensitive_by_default": "Implicite marki afiÅojn konsternaj", + "reply_visibility_following_short": "Montri respondojn por miaj abonatoj", + "hide_all_muted_posts": "KaÅi silentigitajn afiÅojn", + "hide_media_previews": "KaÅi antaÅrigardojn al vidaÅdaĵoj", + "word_filter": "Vortofiltro", + "reply_visibility_self_short": "Montri nur respondojn por mi", + "file_export_import": { + "errors": { + "file_slightly_new": "Etversio de dosiero malsamas, iuj agordoj eble ne funkcios", + "file_too_old": "Nekonforma ĉefa versio: {fileMajor}, versio de dosiero estas tro malnova kaj nesubtenata (minimuma estas {feMajor})", + "file_too_new": "Nekonforma ĉefa versio: {fileMajor}, ĉi tiu PleromaFE (agordoj je versio {feMajor}) tro malnovas por tio", + "invalid_file": "La elektita dosiero ne estas subtenata savkopio de agordoj de Pleroma. Nenio ÅanÄiÄis." + }, + "restore_settings": "Rehavi agordojn el dosiero", + "backup_settings_theme": "Savkopii agordojn kaj haÅton al dosiero", + "backup_settings": "Savkopii agordojn al dosiero", + "backup_restore": "Savkopio de agordoj" + }, + "right_sidebar": "Montri flankan breton dekstre", + "save": "Konservi ÅanÄojn", + "hide_shoutbox": "KaÅi kriujon de nodo", + "always_show_post_button": "Ĉiam montri Åvebantan butonon por nova afiÅo", + "mentions_new_style": "Pli mojosaj menciligiloj", + "mentions_new_place": "Meti menciojn sur apartan linion" }, "timeline": { "collapse": "Maletendi", @@ -530,7 +570,9 @@ "no_more_statuses": "Neniuj pliaj statoj", "no_statuses": "Neniuj statoj", "reload": "Enlegi ree", - "error": "Eraris akirado de historio: {0}" + "error": "Eraris akirado de historio: {0}", + "socket_reconnected": "Realtempa konekto fariÄis", + "socket_broke": "Realtempa konekto perdiÄis: CloseEvent code {0}" }, "user_card": { "approve": "Aprobi", @@ -541,7 +583,6 @@ "follow": "Aboni", "follow_sent": "Peto sendiÄis!", "follow_progress": "Petante…", - "follow_again": "Ĉu sendi peton ree?", "follow_unfollow": "Malaboni", "followees": "Abonatoj", "followers": "Abonantoj", @@ -586,7 +627,15 @@ "show_repeats": "Montri ripetojn", "hide_repeats": "KaÅi ripetojn", "unsubscribe": "Ne ricevi sciigojn", - "subscribe": "Ricevi sciigojn" + "subscribe": "Ricevi sciigojn", + "bot": "Roboto", + "highlight": { + "side": "Flanka strio", + "striped": "Stria fono", + "solid": "Unueca fono", + "disabled": "Senemfaze" + }, + "edit_profile": "Redakti profilon" }, "user_profile": { "timeline_title": "Historio de uzanto", @@ -612,7 +661,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 +695,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.", @@ -670,7 +722,7 @@ "media_nsfw": "Devige marki vidaÅdaĵojn konsternaj", "media_removal_desc": "Ĉi tiu nodo forigas vidaÅdaĵojn de afiÅoj el la jenaj nodoj:", "media_removal": "Forigo de vidaÅdaĵoj", - "ftl_removal": "Forigo el la historio de «La tuta konata reto»", + "ftl_removal": "Forigo el la historio de «Konata reto»", "quarantine_desc": "Ĉi tiu nodo sendos nur publikajn aï¬Åojn al la jenaj nodoj:", "quarantine": "Kvaranteno", "reject_desc": "Ĉi tiu nodo ne akceptos mesaÄojn de la jenaj nodoj:", @@ -678,7 +730,7 @@ "accept_desc": "Ĉi tiu nodo nur akceptas mesaÄojn de la jenaj nodoj:", "accept": "Akcepti", "simple_policies": "Specialaj politikoj de la nodo", - "ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «La tuta konata reto»:" + "ftl_removal_desc": "Ĉi tiu nodo forigas la jenajn nodojn el la historio de «Konata reto»:" }, "mrf_policies": "Åœaltis politikon de MesaÄa ÅanÄilaro (MRF)", "keyword": { @@ -732,7 +784,12 @@ "repeats": "Ripetoj", "favorites": "Åœatoj", "status_deleted": "Ĉi tiu afiÅo foriÄis", - "nsfw": "Konsterna" + "nsfw": "Konsterna", + "expand": "Etendi", + "external_source": "Ekstera fonto", + "mentions": "Mencioj", + "you": "(Vi)", + "plus_more": "+{number} pli" }, "time": { "years_short": "{0}j", diff --git a/src/i18n/es.json b/src/i18n/es.json index f4d87eb3e50a60f06fe189d1fe0fcf3f69c38b3e..eb9fc0a5c0d9659d6394f631770cdabd8e466ddf 100644 --- a/src/i18n/es.json +++ b/src/i18n/es.json @@ -34,12 +34,19 @@ "enable": "Habilitar", "confirm": "Confirmar", "verify": "Verificar", - "peek": "Ojear", + "peek": "Previsualizar", "close": "Cerrar", "dismiss": "Descartar", "retry": "Inténtalo de nuevo", "error_retry": "Por favor, inténtalo de nuevo", - "loading": "Cargando…" + "loading": "Cargando…", + "role": { + "admin": "Administrador/a", + "moderator": "Moderador/a" + }, + "flash_content": "Haga clic para mostrar contenido Flash usando Ruffle (experimental, puede que no funcione).", + "flash_security": "Tenga en cuenta que esto puede ser potencialmente peligroso ya que el contenido Flash sigue siendo código arbitrario.", + "flash_fail": "No se pudo cargar el contenido flash, consulte la consola para obtener más detalles." }, "image_cropper": { "crop_picture": "Recortar la foto", @@ -82,8 +89,8 @@ "friend_requests": "Solicitudes de seguimiento", "mentions": "Menciones", "interactions": "Interacciones", - "dms": "Mensajes Directos", - "public_tl": "LÃnea Temporal Pública", + "dms": "Mensajes directos", + "public_tl": "LÃnea temporal pública", "timeline": "LÃnea Temporal", "twkn": "Red Conocida", "user_search": "Búsqueda de Usuarios", @@ -92,7 +99,8 @@ "preferences": "Preferencias", "chats": "Chats", "timelines": "LÃneas de Tiempo", - "bookmarks": "Marcadores" + "bookmarks": "Marcadores", + "home_timeline": "LÃnea temporal personal" }, "notifications": { "broken_favorite": "Estado desconocido, buscándolo…", @@ -120,7 +128,9 @@ "expiry": "Tiempo de vida de la encuesta", "expires_in": "La encuesta termina en {0}", "expired": "La encuesta terminó hace {0}", - "not_enough_options": "Muy pocas opciones únicas en la encuesta" + "not_enough_options": "Muy pocas opciones únicas en la encuesta", + "people_voted_count": "{count} persona votó | {count} personas votaron", + "votes_count": "{count} voto | {count} votos" }, "emoji": { "stickers": "Pegatinas", @@ -137,14 +147,14 @@ "add_sticker": "Añadir Pegatina" }, "interactions": { - "favs_repeats": "Favoritos y Repetidos", + "favs_repeats": "Favoritos y repetidos", "follows": "Nuevos seguidores", "load_older": "Cargar interacciones más antiguas", - "moves": "Usuario Migrado" + "moves": "Usuario migrado" }, "post_status": { "new_status": "Publicar un nuevo estado", - "account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las entradas para Solo-Seguidores.", + "account_not_locked_warning": "Tu cuenta no está {0}. Cualquiera puede seguirte y leer las publicaciones para Solo-Seguidores.", "account_not_locked_warning_link": "bloqueada", "attachments_sensitive": "Contenido sensible", "content_type": { @@ -164,16 +174,17 @@ "unlisted": "Esta publicación no será visible en la LÃnea Temporal Pública ni en Toda La Red Conocida" }, "scope": { - "direct": "Directo - Solo para los usuarios mencionados", - "private": "Solo-seguidores - Solo tus seguidores leerán la publicación", - "public": "Público - Entradas visibles en las LÃneas Temporales Públicas", - "unlisted": "Sin listar - Entradas no visibles en las LÃneas Temporales Públicas" + "direct": "Directo - solo para los usuarios mencionados", + "private": "Solo-seguidores - solo tus seguidores leerán la publicación", + "public": "Público - publicaciones visibles en las lÃneas temporales públicas", + "unlisted": "Sin listar -publicaciones no visibles en las lÃneas temporales públicas" }, "media_description_error": "Error al actualizar el archivo, inténtalo de nuevo", "empty_status_error": "No se puede publicar un estado vacÃo y sin archivos adjuntos", "preview_empty": "VacÃo", "preview": "Vista previa", - "media_description": "Descripción multimedia" + "media_description": "Descripción multimedia", + "post": "Publicar" }, "registration": { "bio": "BiografÃa", @@ -194,7 +205,10 @@ "password_required": "no puede estar vacÃo", "password_confirmation_required": "no puede estar vacÃo", "password_confirmation_match": "la contraseña no coincide" - } + }, + "reason_placeholder": "Los registros de esta instancia son aprobados manualmente.\nComéntanos por qué quieres registrarte aquÃ.", + "reason": "Razón para registrarse", + "register": "Registrarse" }, "selectable_list": { "select_all": "Seleccionar todo" @@ -227,7 +241,7 @@ "attachmentRadius": "Adjuntos", "attachments": "Adjuntos", "avatar": "Avatar", - "avatarAltRadius": "Avatares (Notificaciones)", + "avatarAltRadius": "Avatares (notificaciones)", "avatarRadius": "Avatares", "background": "Fondo", "bio": "BiografÃa", @@ -245,19 +259,19 @@ "change_password": "Cambiar contraseña", "change_password_error": "Hubo un problema cambiando la contraseña.", "changed_password": "¡Contraseña cambiada correctamente!", - "collapse_subject": "Colapsar entradas con tema", + "collapse_subject": "Colapsar publicaciones con tema", "composing": "Redactando", "confirm_new_password": "Confirmar la nueva contraseña", "current_avatar": "Tu avatar actual", "current_password": "Contraseña actual", "current_profile_banner": "Tu cabecera actual", - "data_import_export_tab": "Importar / Exportar Datos", + "data_import_export_tab": "Importar / Exportar datos", "default_vis": "Alcance de visibilidad por defecto", "delete_account": "Eliminar la cuenta", "discoverable": "Permitir la aparición de esta cuenta en los resultados de búsqueda y otros servicios", "delete_account_description": "Eliminar para siempre los datos y desactivar la cuenta.", "pad_emoji": "Rellenar con espacios al agregar emojis desde el selector", - "delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.", + "delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el/la administrador/a de tu instancia.", "delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.", "avatar_size_instruction": "El tamaño mÃnimo recomendado para el avatar es de 150X150 pÃxeles.", "export_theme": "Exportar tema", @@ -277,7 +291,7 @@ "hide_isp": "Ocultar el panel especÃfico de la instancia", "preload_images": "Precargar las imágenes", "use_one_click_nsfw": "Abrir los adjuntos NSFW con un solo click", - "hide_post_stats": "Ocultar las estadÃsticas de las entradas (p.ej. el número de favoritos)", + "hide_post_stats": "Ocultar las estadÃsticas de las publicaciones (p.ej. el número de favoritos)", "hide_user_stats": "Ocultar las estadÃsticas del usuario (p.ej. el número de seguidores)", "hide_filtered_statuses": "Ocultar estados filtrados", "import_blocks_from_a_csv_file": "Importar lista de usuarios bloqueados dese un archivo csv", @@ -299,22 +313,22 @@ "play_videos_in_modal": "Reproducir los vÃdeos en un marco emergente", "use_contain_fit": "No recortar los adjuntos en miniaturas", "name": "Nombre", - "name_bio": "Nombre y BiografÃa", + "name_bio": "Nombre y biografÃa", "new_password": "Nueva contraseña", "notification_visibility": "Tipos de notificaciones a mostrar", "notification_visibility_follows": "Nuevos seguidores", - "notification_visibility_likes": "Me gustan (Likes)", + "notification_visibility_likes": "Favoritos", "notification_visibility_mentions": "Menciones", "notification_visibility_repeats": "Repeticiones (Repeats)", - "no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las entradas", + "no_rich_text_description": "Eliminar el formato de texto enriquecido de todas las publicaciones", "no_blocks": "No hay usuarios bloqueados", "no_mutes": "No hay usuarios silenciados", "hide_follows_description": "No mostrar a quién sigo", "hide_followers_description": "No mostrar quién me sigue", "hide_follows_count_description": "No mostrar el número de cuentas que sigo", "hide_followers_count_description": "No mostrar el número de cuentas que me siguen", - "show_admin_badge": "Mostrar la insignia de Administrador en mi perfil", - "show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil", + "show_admin_badge": "Mostrar la insignia de \"Administrador/a\" en mi perfil", + "show_moderator_badge": "Mostrar la insignia de \"Moderador/a\" en mi perfil", "nsfw_clickthrough": "Habilitar la ocultación de la imagen de vista previa del enlace y el adjunto para los estados NSFW por defecto", "oauth_tokens": "Tokens de OAuth", "token": "Token", @@ -324,8 +338,8 @@ "panelRadius": "Paneles", "pause_on_unfocused": "Parar la transmisión cuando no estés en foco", "presets": "Por defecto", - "profile_background": "Fondo del Perfil", - "profile_banner": "Cabecera del Perfil", + "profile_background": "Imagen de fondo del perfil", + "profile_banner": "Imagen de cabecera del perfil", "profile_tab": "Perfil", "radii_help": "Establezca el redondeo de las esquinas de la interfaz (en pÃxeles)", "replies_in_timeline": "Réplicas en la lÃnea temporal", @@ -356,7 +370,7 @@ "theme": "Tema", "theme_help": "Use códigos de color hexadecimales (#rrggbb) para personalizar su tema de colores.", "theme_help_v2_1": "También puede invalidar los colores y la opacidad de ciertos componentes si activa la casilla de verificación. Use el botón \"Borrar todo\" para deshacer los cambios.", - "theme_help_v2_2": "Los iconos debajo de algunas entradas son indicadores de contraste de fondo/texto, desplace el ratón por encima para obtener información más detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.", + "theme_help_v2_2": "Los iconos debajo de algunas publicaciones son indicadores de contraste de fondo/texto, desplace el ratón por encima para obtener información más detallada. Tenga en cuenta que cuando se utilizan indicadores de contraste de transparencia se muestra el peor caso posible.", "tooltipRadius": "Información/alertas", "upload_a_photo": "Subir una foto", "user_settings": "Ajustes del Usuario", @@ -476,7 +490,7 @@ "panelHeader": "Cabecera del panel", "topBar": "Barra superior", "avatar": "Avatar del usuario (en la vista del perfil)", - "avatarStatus": "Avatar del usuario (en la vista de la entrada)", + "avatarStatus": "Avatar del usuario (en la vista de la publicación)", "popup": "Ventanas y textos emergentes (popups & tooltips)", "button": "Botones", "buttonHover": "Botón (encima)", @@ -517,8 +531,8 @@ }, "version": { "title": "Versión", - "backend_version": "Versión del Backend", - "frontend_version": "Versión del Frontend" + "backend_version": "Versión del backend", + "frontend_version": "Versión del frontend" }, "notification_visibility_moves": "Usuario Migrado", "greentext": "Texto verde (meme arrows)", @@ -529,7 +543,7 @@ "fun": "Divertido", "type_domains_to_mute": "Buscar dominios para silenciar", "useStreamingApiWarning": "(no recomendado, experimental, puede omitir publicaciones)", - "useStreamingApi": "Recibir entradas y notificaciones en tiempo real", + "useStreamingApi": "Recibir publicaciones y notificaciones en tiempo real", "user_mutes": "Usuarios", "reset_profile_background": "Restablecer el fondo de pantalla", "reset_background_confirm": "¿Estás seguro de restablecer el fondo de pantalla?", @@ -562,7 +576,33 @@ "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", + "hide_all_muted_posts": "Ocultar las publicaciones silenciadas", + "more_settings": "Más opciones", + "sensitive_by_default": "Identificar las publicaciones como sensibles de forma predeterminada", + "reply_visibility_self_short": "Mostrar respuestas solo a uno mismo", + "reply_visibility_following_short": "Mostrar las réplicas a mis seguidores", + "hide_media_previews": "Ocultar la vista previa multimedia", + "word_filter": "Filtro de palabras", + "save": "Guardar los cambios", + "file_export_import": { + "errors": { + "invalid_file": "El archivo seleccionado no es válido como copia de seguridad de Pleroma. No se han realizado cambios.", + "file_too_new": "Versión principal incompatible: {fileMajor}, este \"FrontEnd\" de Pleroma (versión de configuración {feMajor}) es demasiado antiguo para manejarlo", + "file_too_old": "Versión principal incompatible: {fileMajor}, la versión del archivo es demasiado antigua y no es compatible (versión mÃnima {FeMajor})", + "file_slightly_new": "La versión secundaria del archivo es diferente, es posible que algunas configuraciones no se carguen" + }, + "restore_settings": "Restaurar ajustes desde archivo", + "backup_settings_theme": "Descargar la copia de seguridad de la configuración y del tema", + "backup_settings": "Descargar la copia de seguridad de la configuración", + "backup_restore": "Copia de seguridad de la configuración" + }, + "hide_shoutbox": "Ocultar cuadro de diálogo de la instancia", + "right_sidebar": "Mostrar la barra lateral a la derecha", + "always_show_post_button": "Muestra siempre el botón flotante de Nueva Plubicación", + "mentions_new_style": "Enlaces de menciones más elegantes", + "mentions_new_place": "Situa las menciones en una lÃnea separada" }, "time": { "day": "{0} dÃa", @@ -610,7 +650,9 @@ "no_more_statuses": "No hay más estados", "no_statuses": "Sin estados", "reload": "Recargar", - "error": "Error obteniendo la linea de tiempo:{0}" + "error": "Error obteniendo la linea de tiempo:{0}", + "socket_broke": "Conexión en timpo real perdida: código del motivo {0}", + "socket_reconnected": "Establecida la conexión en tiempo real" }, "status": { "favorites": "Favoritos", @@ -634,10 +676,13 @@ "status_unavailable": "Estado no disponible", "bookmark": "Marcar", "unbookmark": "Desmarcar", - "status_deleted": "Esta entrada ha sido eliminada", + "status_deleted": "Esta publicación ha sido eliminada", "nsfw": "NSFW (No apropiado para el trabajo)", "expand": "Expandir", - "external_source": "Fuente externa" + "external_source": "Fuente externa", + "mentions": "Menciones", + "you": "(Tú)", + "plus_more": "+{number} más" }, "user_card": { "approve": "Aprobar", @@ -648,7 +693,6 @@ "follow": "Seguir", "follow_sent": "¡Solicitud enviada!", "follow_progress": "Solicitando…", - "follow_again": "¿Enviar solicitud de nuevo?", "follow_unfollow": "Dejar de seguir", "followees": "Siguiendo", "followers": "Seguidores", @@ -673,10 +717,10 @@ "mute_progress": "Silenciando…", "admin_menu": { "moderation": "Moderación", - "grant_admin": "Conceder permisos de Administrador", - "revoke_admin": "Revocar permisos de Administrador", - "grant_moderator": "Conceder permisos de Moderador", - "revoke_moderator": "Revocar permisos de Moderador", + "grant_admin": "Conceder permisos de Administrador/a", + "revoke_admin": "Revocar permisos de Administrador/a", + "grant_moderator": "Conceder permisos de Moderador/a", + "revoke_moderator": "Revocar permisos de Moderador/a", "activate_account": "Activar cuenta", "deactivate_account": "Desactivar cuenta", "delete_account": "Eliminar cuenta", @@ -693,16 +737,28 @@ "show_repeats": "Mostrar repetidos", "hide_repeats": "Ocultar repetidos", "message": "Mensaje", - "hidden": "Oculto" + "hidden": "Oculto", + "roles": { + "moderator": "Moderador", + "admin": "Administrador" + }, + "highlight": { + "striped": "Fondo rayado", + "side": "Raya lateral", + "solid": "Fondo sólido", + "disabled": "Sin resaltado" + }, + "bot": "Bot", + "edit_profile": "Edita el perfil" }, "user_profile": { - "timeline_title": "Linea Temporal del Usuario", + "timeline_title": "LÃnea temporal del usuario", "profile_does_not_exist": "Lo sentimos, este perfil no existe.", "profile_loading_error": "Lo sentimos, hubo un error al cargar este perfil." }, "user_reporting": { "title": "Reportando a {0}", - "add_comment_description": "El informe será enviado a los moderadores de su instancia. Puedes proporcionar una explicación de por qué estás reportando esta cuenta a continuación:", + "add_comment_description": "El informe será enviado a los/las moderadores/as de su instancia. Puedes proporcionar una explicación de por qué estás reportando esta cuenta a continuación:", "additional_comments": "Comentarios adicionales", "forward_description": "La cuenta es de otro servidor. ¿Enviar una copia del informe allà también?", "forward_to": "Reenviar a {0}", @@ -714,7 +770,7 @@ "who_to_follow": "A quién seguir" }, "tool_tip": { - "media_upload": "Subir Medios", + "media_upload": "Subir multimedia", "repeat": "Repetir", "reply": "Contestar", "favorite": "Favorito", @@ -772,12 +828,12 @@ "simple": { "accept_desc": "Esta instancia solo acepta mensajes de las siguientes instancias:", "media_nsfw_desc": "Esta instancia obliga a que los archivos multimedia se establezcan como sensibles en las publicaciones de las siguientes instancias:", - "media_nsfw": "Forzar Multimedia Como Sensible", + "media_nsfw": "Forzar contenido multimedia como sensible", "media_removal_desc": "Esta instancia elimina los archivos multimedia de las publicaciones de las siguientes instancias:", "media_removal": "Eliminar Multimedia", "quarantine": "Cuarentena", - "ftl_removal_desc": "Esta instancia elimina las siguientes instancias de la lÃnea de tiempo \"Toda la red conocida\":", - "ftl_removal": "Eliminar de la lÃnea de tiempo \"Toda La Red Conocida\"", + "ftl_removal_desc": "Esta instancia elimina las siguientes instancias de la lÃnea de tiempo \"Red Conocida\":", + "ftl_removal": "Eliminar de la lÃnea de tiempo \"Red Conocida\"", "quarantine_desc": "Esta instancia enviará solo publicaciones públicas a las siguientes instancias:", "simple_policies": "PolÃticas especÃficas de la instancia", "reject_desc": "Esta instancia no aceptará mensajes de las siguientes instancias:", diff --git a/src/i18n/eu.json b/src/i18n/eu.json index a45b7cfd767db1c7bce5b5ef3f16c31362fc5a02..539ee1bdb68a39140fa3f65a52ec2668441175f2 100644 --- a/src/i18n/eu.json +++ b/src/i18n/eu.json @@ -14,7 +14,8 @@ "text_limit": "Testu limitea", "title": "Ezaugarriak", "who_to_follow": "Nori jarraitu", - "pleroma_chat_messages": "Pleroma Txata" + "pleroma_chat_messages": "Pleroma Txata", + "upload_limit": "Kargatzeko muga" }, "finder": { "error_fetching_user": "Errorea erabiltzailea eskuratzen", @@ -38,7 +39,14 @@ "dismiss": "Baztertu", "retry": "Saiatu berriro", "error_retry": "Saiatu berriro mesedez", - "loading": "Kargatzen…" + "loading": "Kargatzen…", + "role": { + "moderator": "Moderatzailea", + "admin": "Administratzailea" + }, + "flash_content": "Klik egin Flash edukia erakusteko Ruffle erabilita (esperimentala, baliteke ez ibiltzea).", + "flash_security": "Kontuan izan arriskutsua izan daitekeela, Flash edukia kode arbitrarioa baita.", + "flash_fail": "Ezin izan da Flash edukia kargatu. Ikusi kontsola xehetasunetarako." }, "image_cropper": { "crop_picture": "Moztu argazkia", @@ -81,8 +89,8 @@ "friend_requests": "Jarraitzeko eskaerak", "mentions": "Aipamenak", "interactions": "Interakzioak", - "dms": "Zuzeneko Mezuak", - "public_tl": "Denbora-lerro Publikoa", + "dms": "Zuzeneko mezuak", + "public_tl": "Denbora-lerro publikoa", "timeline": "Denbora-lerroa", "twkn": "Ezagutzen den Sarea", "user_search": "Erabiltzailea Bilatu", @@ -91,7 +99,8 @@ "preferences": "Hobespenak", "chats": "Txatak", "timelines": "Denbora-lerroak", - "bookmarks": "Laster-markak" + "bookmarks": "Laster-markak", + "home_timeline": "Denbora-lerro pertsonala" }, "notifications": { "broken_favorite": "Egoera ezezaguna, bilatzen…", @@ -104,7 +113,8 @@ "no_more_notifications": "Ez dago jakinarazpen gehiago", "reacted_with": "{0}kin erreakzionatu zuen", "migrated_to": "hona migratua:", - "follow_request": "jarraitu nahi zaitu" + "follow_request": "jarraitu nahi zaitu", + "error": "Errorea jakinarazpenak eskuratzean: {0}" }, "polls": { "add_poll": "Inkesta gehitu", @@ -118,7 +128,9 @@ "expiry": "Inkestaren iraupena", "expires_in": "Inkesta {0} bukatzen da", "expired": "Inkesta {0} bukatu zen", - "not_enough_options": "Aukera gutxiegi inkestan" + "not_enough_options": "Aukera gutxiegi inkestan", + "votes_count": "{count} boto| {count} boto", + "people_voted_count": "Pertsona batek bozkatu du | {count} pertsonak bozkatu dute" }, "emoji": { "stickers": "Pegatinak", @@ -128,7 +140,8 @@ "add_emoji": "Emoji bat gehitu", "custom": "Ohiko emojiak", "unicode": "Unicode emojiak", - "load_all": "{emojiAmount} emoji guztiak kargatzen" + "load_all": "{emojiAmount} emoji guztiak kargatzen", + "load_all_hint": "Lehenengo {saneAmount} emojia kargatuta, emoji guztiak kargatzeak errendimendu arazoak sor ditzake." }, "stickers": { "add_sticker": "Pegatina gehitu" @@ -136,7 +149,8 @@ "interactions": { "favs_repeats": "Errepikapen eta gogokoak", "follows": "Jarraitzaile berriak", - "load_older": "Kargatu elkarrekintza zaharragoak" + "load_older": "Kargatu elkarrekintza zaharragoak", + "moves": "Erabiltzailea migratuta" }, "post_status": { "new_status": "Mezu berri bat idatzi", @@ -160,18 +174,24 @@ "unlisted": "Mezu hau ez da argitaratuko Denbora-lerro Publikoan ezta Ezagutzen den Sarean" }, "scope": { - "direct": "Zuzena: Bidali aipatutako erabiltzaileei besterik ez", - "private": "Jarraitzaileentzako bakarrik: Bidali jarraitzaileentzat bakarrik", - "public": "Publikoa: Bistaratu denbora-lerro publikoetan", + "direct": "Zuzena: bidali aipatutako erabiltzaileei besterik ez", + "private": "Jarraitzaileentzako bakarrik: bidali jarraitzaileentzat bakarrik", + "public": "Publikoa: bistaratu denbora-lerro publikoetan", "unlisted": "Zerrendatu gabea: ez bidali denbora-lerro publikoetara" - } + }, + "media_description_error": "Ezin izan da artxiboa eguneratu, saiatu berriro", + "preview": "Aurrebista", + "media_description": "Media deskribapena", + "preview_empty": "Hutsik", + "post": "Bidali", + "empty_status_error": "Ezin da argitaratu ezer idatzi gabe edo eranskinik gabe" }, "registration": { "bio": "Biografia", "email": "E-posta", "fullname": "Erakutsi izena", "password_confirm": "Pasahitza berretsi", - "registration": "Izena ematea", + "registration": "Sortu kontua", "token": "Gonbidapen txartela", "captcha": "CAPTCHA", "new_captcha": "Klikatu irudia captcha berri bat lortzeko", @@ -185,7 +205,10 @@ "password_required": "Ezin da hutsik utzi", "password_confirmation_required": "Ezin da hutsik utzi", "password_confirmation_match": "Pasahitzaren berdina izan behar du" - } + }, + "reason": "Kontua sortzeko arrazoia", + "reason_placeholder": "Instantzia honek kontu berriak eskuz onartzen ditu.\nJakinarazi administrazioari zergatik erregistratu nahi duzun.", + "register": "Erregistratu" }, "selectable_list": { "select_all": "Hautatu denak" @@ -202,7 +225,7 @@ "title": "Bi-faktore autentifikazioa", "generate_new_recovery_codes": "Sortu berreskuratze kode berriak", "warning_of_generate_new_codes": "Berreskuratze kode berriak sortzean, zure berreskuratze kode zaharrak ez dute balioko.", - "recovery_codes": "Berreskuratze kodea", + "recovery_codes": "Berreskuratze kodea.", "waiting_a_recovery_codes": "Babes-kopia kodeak jasotzen…", "recovery_codes_warning": "Idatzi edo gorde kodeak leku seguruan - bestela ez dituzu berriro ikusiko. Zure 2FA aplikaziorako sarbidea eta berreskuratze kodeak galduz gero, zure kontutik blokeatuta egongo zara.", "authentication_methods": "Autentifikazio metodoa", @@ -218,7 +241,7 @@ "attachmentRadius": "Eranskinak", "attachments": "Eranskinak", "avatar": "Avatarra", - "avatarAltRadius": "Avatarra (Aipamenak)", + "avatarAltRadius": "Abatarra (aipamenak)", "avatarRadius": "Avatarrak", "background": "Atzeko planoa", "bio": "Biografia", @@ -242,7 +265,7 @@ "current_avatar": "Zure uneko avatarra", "current_password": "Indarrean dagoen pasahitza", "current_profile_banner": "Zure profilaren banner-a", - "data_import_export_tab": "Datuak Inportatu / Esportatu", + "data_import_export_tab": "Datuak inportatu / esportatu", "default_vis": "Lehenetsitako ikusgaitasunak", "delete_account": "Ezabatu kontua", "discoverable": "Baimendu zure kontua kanpo bilaketa-emaitzetan eta bestelako zerbitzuetan agertzea", @@ -304,19 +327,19 @@ "hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen", "hide_follows_count_description": "Ez erakutsi jarraitzen ari naizen kontuen kopurua", "hide_followers_count_description": "Ez erakutsi nire jarraitzaileen kontuen kopurua", - "show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan", - "show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan", + "show_admin_badge": "Erakutsi \"Administratzaile\" etiketa nire profilan", + "show_moderator_badge": "Erakutsi \"Moderatzaile\" etiketa nire profilan", "nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko", "oauth_tokens": "OAuth tokenak", "token": "Tokena", - "refresh_token": "Berrgin Tokena", - "valid_until": "Baliozkoa Arte", + "refresh_token": "Berrgin tokena", + "valid_until": "Baliozkoa arte", "revoke_token": "Ezeztatu", "panelRadius": "Panelak", "pause_on_unfocused": "Eguneraketa automatikoa gelditu fitxatik kanpo", "presets": "Aurrezarpenak", "profile_background": "Profilaren atzeko planoa", - "profile_banner": "Profilaren Banner-a", + "profile_banner": "Profilaren banner-a", "profile_tab": "Profila", "radii_help": "Konfiguratu interfazearen ertzen biribiltzea (pixeletan)", "replies_in_timeline": "Denbora-lerroko erantzunak", @@ -460,7 +483,7 @@ "button": "Botoia", "text": "Hamaika {0} eta {1}", "mono": "edukia", - "input": "Jadanik Los Angeles-en", + "input": "Jadanik Los Angeles-en.", "faint_link": "laguntza", "fine_print": "Irakurri gure {0} ezer erabilgarria ikasteko!", "header_faint": "Ondo dago", @@ -470,9 +493,13 @@ }, "version": { "title": "Bertsioa", - "backend_version": "Backend Bertsioa", - "frontend_version": "Frontend Bertsioa" - } + "backend_version": "Backend bertsioa", + "frontend_version": "Frontend bertsioa" + }, + "save": "Aldaketak gorde", + "setting_changed": "Ezarpena lehenetsitakoaren desberdina da", + "allow_following_move": "Baimendu jarraipen automatikoa, jarraitzen duzun kontua beste instantzia batera eramaten denean", + "new_email": "E-posta berria" }, "time": { "day": "{0} egun", @@ -542,7 +569,6 @@ "follow": "Jarraitu", "follow_sent": "Eskaera bidalita!", "follow_progress": "Eskatzen…", - "follow_again": "Eskaera berriro bidali?", "follow_unfollow": "Jarraitzeari utzi", "followees": "Jarraitzen", "followers": "Jarraitzaileak", @@ -657,7 +683,7 @@ "federation": "Federazioa", "simple": { "media_nsfw_desc": "Instantzia honek hurrengo instantzien multimediak sentikorrak izatera behartzen ditu:", - "media_nsfw": "Behartu Multimedia Sentikor", + "media_nsfw": "Behartu multimedia sentikor moduan", "media_removal_desc": "Instantzia honek atxikitutako multimedia hurrengo instantzietatik ezabatzen ditu:", "media_removal": "Multimedia Ezabatu", "ftl_removal_desc": "Instantzia honek hurrengo instantziak ezabatzen ditu \"Ezagutzen den Sarea\" denbora-lerrotik:", @@ -683,5 +709,12 @@ }, "shoutbox": { "title": "Oihu-kutxa" + }, + "errors": { + "storage_unavailable": "Pleromak ezin izan du nabigatzailearen biltegira sartu. Hasiera-saioa edo tokiko ezarpenak ez dira gordeko eta ustekabeko arazoak sor ditzake. Saiatu cookie-ak gaitzen." + }, + "remote_user_resolver": { + "searching_for": "Bilatzen", + "error": "Ez da aurkitu." } } diff --git a/src/i18n/fi.json b/src/i18n/fi.json index 2524f2788a5223943abef95d834f9441d27465a3..7b5244cb7cf456e53e3b542470eeb21e474554ba 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -579,7 +579,8 @@ "hide_full_subject": "Piilota koko otsikko", "show_content": "Näytä sisältö", "hide_content": "Piilota sisältö", - "status_deleted": "Poistettu viesti" + "status_deleted": "Poistettu viesti", + "you": "(sinä)" }, "user_card": { "approve": "Hyväksy", @@ -589,7 +590,6 @@ "follow": "Seuraa", "follow_sent": "Pyyntö lähetetty!", "follow_progress": "Pyydetään…", - "follow_again": "Lähetä pyyntö uudestaan?", "follow_unfollow": "Älä seuraa", "followees": "Seuraa", "followers": "Seuraajat", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 63ad46d2cd1ca61202f1ade537d61f186721b418..6d3c75d163a0d5278639e089b5d45acfe1d30572 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -9,16 +9,17 @@ "features_panel": { "chat": "Chat", "gopher": "Gopher", - "media_proxy": "Proxy média", + "media_proxy": "Proxy pièce-jointes", "scope_options": "Options de visibilité", - "text_limit": "Limite de texte", - "title": "Caractéristiques", - "who_to_follow": "Personnes à suivre", - "pleroma_chat_messages": "Chat Pleroma" + "text_limit": "Limite du texte", + "title": "Fonctionnalités", + "who_to_follow": "Suggestions de suivis", + "pleroma_chat_messages": "Chat Pleroma", + "upload_limit": "Limite de téléversement" }, "finder": { - "error_fetching_user": "Erreur lors de la recherche de l'utilisateur·ice", - "find_user": "Chercher un-e utilisateur·ice" + "error_fetching_user": "Erreur lors de la recherche du compte", + "find_user": "Rechercher un compte" }, "general": { "apply": "Appliquer", @@ -26,19 +27,26 @@ "more": "Plus", "generic_error": "Une erreur s'est produite", "optional": "optionnel", - "show_more": "Montrer plus", - "show_less": "Montrer moins", + "show_more": "Afficher plus", + "show_less": "Afficher moins", "cancel": "Annuler", "disable": "Désactiver", "enable": "Activer", "confirm": "Confirmer", "verify": "Vérifier", - "dismiss": "Rejeter", + "dismiss": "Ignorer", "peek": "Jeter un coup d'Å“il", "close": "Fermer", "retry": "Réessayez", "error_retry": "Veuillez réessayer", - "loading": "Chargement…" + "loading": "Chargement…", + "role": { + "moderator": "Modo'", + "admin": "Admin" + }, + "flash_content": "Clique pour afficher le contenu Flash avec Ruffle (Expérimental, peut ne pas fonctionner).", + "flash_security": "Cela reste potentiellement dangereux, Flash restant du code arbitraire.", + "flash_fail": "Échec de chargement du contenu Flash, voir la console pour les détails." }, "image_cropper": { "crop_picture": "Rogner l'image", @@ -47,7 +55,7 @@ "cancel": "Annuler" }, "importer": { - "submit": "Soumettre", + "submit": "Envoyer", "success": "Importé avec succès.", "error": "Une erreur est survenue pendant l'import de ce fichier." }, @@ -56,17 +64,17 @@ "description": "Connexion avec OAuth", "logout": "Déconnexion", "password": "Mot de passe", - "placeholder": "p.e. lain", + "placeholder": "ex. lain", "register": "S'inscrire", "username": "Identifiant", "hint": "Connectez-vous pour rejoindre la discussion", "authentication_code": "Code d'authentification", "enter_recovery_code": "Entrez un code de récupération", - "enter_two_factor_code": "Entrez un code à double authentification", + "enter_two_factor_code": "Entrez un code double-facteur", "recovery_code": "Code de récupération", "heading": { - "totp": "Authentification à double authentification", - "recovery": "Récuperation de la double authentification" + "totp": "Authentification à double-facteur", + "recovery": "Récupération de l'authentification à double-facteur" } }, "media_modal": { @@ -78,24 +86,26 @@ "back": "Retour", "chat": "Chat local", "friend_requests": "Demandes de suivi", - "mentions": "Notifications", + "mentions": "Mentions", "interactions": "Interactions", "dms": "Messages directs", - "public_tl": "Fil d'actualité public", - "timeline": "Fil d'actualité", + "public_tl": "Flux publique", + "timeline": "Flux personnel", "twkn": "Réseau connu", - "user_search": "Recherche d'utilisateur·ice", - "who_to_follow": "Qui suivre", + "user_search": "Recherche de comptes", + "who_to_follow": "Suggestion de suivit", "preferences": "Préférences", "search": "Recherche", "administration": "Administration", "chats": "Chats", - "bookmarks": "Marques-Pages" + "bookmarks": "Marques-Pages", + "timelines": "Flux", + "home_timeline": "Flux personnel" }, "notifications": { - "broken_favorite": "Message inconnu, chargement…", + "broken_favorite": "Message inconnu, recherche en cours…", "favorited_you": "a aimé votre statut", - "followed_you": "a commencé à vous suivre", + "followed_you": "vous suit", "load_older": "Charger les notifications précédentes", "notifications": "Notifications", "read": "Lu !", @@ -103,7 +113,8 @@ "no_more_notifications": "Aucune notification supplémentaire", "migrated_to": "a migré à ", "reacted_with": "a réagi avec {0}", - "follow_request": "veut vous suivre" + "follow_request": "veut vous suivre", + "error": "Erreur de chargement des notifications : {0}" }, "interactions": { "favs_repeats": "Partages et favoris", @@ -115,7 +126,7 @@ "new_status": "Poster un nouveau statut", "account_not_locked_warning": "Votre compte n'est pas {0}. N'importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.", "account_not_locked_warning_link": "verrouillé", - "attachments_sensitive": "Marquer le média comme sensible", + "attachments_sensitive": "Marquer les pièce-jointes comme sensible", "content_type": { "text/plain": "Texte brut", "text/html": "HTML", @@ -130,32 +141,33 @@ "scope_notice": { "public": "Ce statut sera visible par tout le monde", "private": "Ce statut sera visible par seulement vos abonné⋅eâ‹…s", - "unlisted": "Ce statut ne sera pas visible dans le Fil d'actualité public et l'Ensemble du réseau connu" + "unlisted": "Ce statut ne sera pas visible dans le Flux Public et le Flux Fédéré" }, "scope": { "direct": "Direct - N'envoyer qu'aux personnes mentionnées", - "private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos billets", - "public": "Publique - Afficher dans les fils publics", - "unlisted": "Non-Listé - Ne pas afficher dans les fils publics" + "private": "Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos status", + "public": "Publique - Afficher dans les flux publics", + "unlisted": "Non-Listé - Ne pas afficher dans les flux publics" }, "media_description_error": "Échec de téléversement du media, essayez encore", - "empty_status_error": "Impossible de poster un statut vide sans attachements", + "empty_status_error": "Impossible de poster un statut vide sans pièces-jointes", "preview_empty": "Vide", "preview": "Prévisualisation", - "media_description": "Description de l'attachement" + "media_description": "Description de la pièce-jointe", + "post": "Post" }, "registration": { "bio": "Biographie", - "email": "Adresse mail", + "email": "Courriel", "fullname": "Pseudonyme", "password_confirm": "Confirmation du mot de passe", "registration": "Inscription", "token": "Jeton d'invitation", "captcha": "CAPTCHA", "new_captcha": "Cliquez sur l'image pour avoir un nouveau captcha", - "username_placeholder": "p.e. lain", - "fullname_placeholder": "p.e. Lain Iwakura", - "bio_placeholder": "p.e.\nSalut, je suis Lain\nJe suis une héroïne d'animé qui vit dans une banlieue japonaise. Vous me connaissez peut-être du Wired.", + "username_placeholder": "ex. lain", + "fullname_placeholder": "ex. Lain Iwakura", + "bio_placeholder": "ex.\nSalut, je suis Lain\nJe suis une héroïne d'animation qui vit dans une banlieue japonaise. Vous me connaissez peut-être du Wired.", "validations": { "username_required": "ne peut pas être laissé vide", "fullname_required": "ne peut pas être laissé vide", @@ -163,7 +175,10 @@ "password_required": "ne peut pas être laissé vide", "password_confirmation_required": "ne peut pas être laissé vide", "password_confirmation_match": "doit être identique au mot de passe" - } + }, + "reason_placeholder": "Cette instance modère les inscriptions manuellement.\nExpliquer ce qui motive votre inscription à l'administration.", + "reason": "Motivation d'inscription", + "register": "Enregistrer" }, "selectable_list": { "select_all": "Tout selectionner" @@ -177,20 +192,20 @@ "setup_otp": "Configurer OTP", "wait_pre_setup_otp": "préconfiguration OTP", "confirm_and_enable": "Confirmer & activer OTP", - "title": "Double authentification", + "title": "Authentification double-facteur", "generate_new_recovery_codes": "Générer de nouveaux codes de récupération", - "warning_of_generate_new_codes": "Quand vous générez de nouveauc codes de récupération, vos anciens codes ne fonctionnerons plus.", + "warning_of_generate_new_codes": "Quand vous générez de nouveaux codes de récupération, vos anciens codes ne fonctionnerons plus.", "recovery_codes": "Codes de récupération.", "waiting_a_recovery_codes": "Réception des codes de récupération…", - "recovery_codes_warning": "Écrivez les codes ou sauvez les quelquepart sécurisé - sinon vous ne les verrez plus jamais. Si vous perdez l'accès à votre application de double authentification et codes de récupération vous serez vérouillé en dehors de votre compte.", - "authentication_methods": "Methodes d'authentification", + "recovery_codes_warning": "Écrivez ces codes ou sauvegardez les dans un endroit sécurisé - sinon vous ne les verrez plus jamais. Si vous perdez l'accès à votre application de double authentification et codes de récupération vous serez verrouillé en dehors de votre compte.", + "authentication_methods": "Méthodes d'authentification", "scan": { "title": "Scanner", - "desc": "En utilisant votre application de double authentification, scannez ce QR code ou entrez la clé textuelle :", + "desc": "En utilisant votre application d'authentification à double-facteur, scannez ce QR code ou entrez la clé textuelle :", "secret_code": "Clé" }, "verify": { - "desc": "Pour activer la double authentification, entrez le code depuis votre application :" + "desc": "Pour activer l'authentification à double-facteur, entrez le code donné par votre application :" } }, "attachmentRadius": "Pièces jointes", @@ -201,10 +216,10 @@ "background": "Arrière-plan", "bio": "Biographie", "block_export": "Export des comptes bloqués", - "block_export_button": "Export des comptes bloqués vers un fichier csv", + "block_export_button": "Export des comptes bloqués vers un fichier CSV", "block_import": "Import des comptes bloqués", "block_import_error": "Erreur lors de l'import des comptes bloqués", - "blocks_imported": "Blocks importés ! Le traitement va prendre un moment.", + "blocks_imported": "Blocages importés ! Le traitement va prendre un moment.", "blocks_tab": "Bloqué·e·s", "btnRadius": "Boutons", "cBlue": "Bleu (répondre, suivre)", @@ -224,31 +239,31 @@ "default_vis": "Visibilité par défaut", "delete_account": "Supprimer le compte", "delete_account_description": "Supprimer définitivement vos données et désactiver votre compte.", - "delete_account_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administrateurâ‹…ice de cette instance.", + "delete_account_error": "Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l'administration de cette instance.", "delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.", "avatar_size_instruction": "La taille minimale recommandée pour l'image de l'avatar est de 150x150 pixels.", "export_theme": "Enregistrer le thème", - "filtering": "Filtre", + "filtering": "Filtrage", "filtering_explanation": "Tous les statuts contenant ces mots seront masqués. Un mot par ligne", - "follow_export": "Exporter les abonnements", - "follow_export_button": "Exporter les abonnements en csv", - "follow_import": "Importer des abonnements", - "follow_import_error": "Erreur lors de l'importation des abonnements", - "follows_imported": "Abonnements importés ! Le traitement peut prendre un moment.", + "follow_export": "Exporter les suivis", + "follow_export_button": "Exporter les suivis dans un fichier CSV", + "follow_import": "Import des suivis", + "follow_import_error": "Erreur lors de l'importation des suivis", + "follows_imported": "Suivis importés ! Le traitement peut prendre un moment.", "foreground": "Premier plan", "general": "Général", "hide_attachments_in_convo": "Masquer les pièces jointes dans les conversations", - "hide_attachments_in_tl": "Masquer les pièces jointes dans le journal", - "hide_muted_posts": "Masquer les statuts des utilisateurs masqués", + "hide_attachments_in_tl": "Masquer les pièces jointes dans le flux", + "hide_muted_posts": "Masquer les statuts des comptes masqués", "max_thumbnails": "Nombre maximum de miniatures par statuts", - "hide_isp": "Masquer le panneau spécifique a l'instance", + "hide_isp": "Masquer le panneau de l'instance", "preload_images": "Précharger les images", - "use_one_click_nsfw": "Ouvrir les pièces-jointes NSFW avec un seul clic", - "hide_post_stats": "Masquer les statistiques de publication (le nombre de favoris)", - "hide_user_stats": "Masquer les statistiques de profil (le nombre d'amis)", + "use_one_click_nsfw": "Ouvrir les pièces-jointes sensibles avec un seul clic", + "hide_post_stats": "Masquer les statistiques des messages (ex. le nombre de favoris)", + "hide_user_stats": "Masquer les statistiques de compte (ex. le nombre de suivis)", "hide_filtered_statuses": "Masquer les statuts filtrés", - "import_blocks_from_a_csv_file": "Importer les blocages depuis un fichier csv", - "import_followers_from_a_csv_file": "Importer des abonnements depuis un fichier csv", + "import_blocks_from_a_csv_file": "Import de blocages depuis un fichier CSV", + "import_followers_from_a_csv_file": "Import de suivis depuis un fichier CSV", "import_theme": "Charger le thème", "inputRadius": "Champs de texte", "checkboxRadius": "Cases à cocher", @@ -269,8 +284,8 @@ "name_bio": "Nom & Bio", "new_password": "Nouveau mot de passe", "notification_visibility": "Types de notifications à afficher", - "notification_visibility_follows": "Abonnements", - "notification_visibility_likes": "J'aime", + "notification_visibility_follows": "Suivis", + "notification_visibility_likes": "Favoris", "notification_visibility_mentions": "Mentionnés", "notification_visibility_repeats": "Partages", "no_rich_text_description": "Ne formatez pas le texte", @@ -278,9 +293,9 @@ "no_mutes": "Aucun masqués", "hide_follows_description": "Ne pas afficher à qui je suis abonné", "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", + "show_admin_badge": "Afficher le badge d'Admin sur mon profil", + "show_moderator_badge": "Afficher le badge de Modo' sur mon profil", + "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", @@ -289,11 +304,11 @@ "panelRadius": "Fenêtres", "pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas actif", "presets": "Thèmes prédéfinis", - "profile_background": "Image de fond", + "profile_background": "Image de fond de profil", "profile_banner": "Bannière de profil", "profile_tab": "Profil", "radii_help": "Vous pouvez ici choisir le niveau d'arrondi des angles de l'interface (en pixels)", - "replies_in_timeline": "Réponses au journal", + "replies_in_timeline": "Réponses dans le flux", "reply_visibility_all": "Montrer toutes les réponses", "reply_visibility_following": "Afficher uniquement les réponses adressées à moi ou aux personnes que je suis", "reply_visibility_self": "Afficher uniquement les réponses adressées à moi", @@ -309,7 +324,7 @@ "set_new_profile_background": "Changer d'image de fond", "set_new_profile_banner": "Changer de bannière", "settings": "Paramètres", - "subject_input_always_show": "Toujours copier le champ de sujet", + "subject_input_always_show": "Toujours afficher le champ Sujet", "subject_line_behavior": "Copier le sujet en répondant", "subject_line_email": "Similaire au courriel : « re : sujet »", "subject_line_mastodon": "Comme mastodon : copier tel quel", @@ -348,7 +363,7 @@ "use_snapshot": "Ancienne version", "help": { "upgraded_from_v2": "PleromaFE à été mis à jour, le thème peut être un peu différent que dans vos souvenirs.", - "v2_imported": "Le fichier que vous avez importé vient d'un version antérieure. Nous essayons de maximizer la compatibilité mais il peu y avoir quelques incohérences.", + "v2_imported": "Le fichier que vous avez importé vient d'une version antérieure. Nous essayons de maximizer la compatibilité mais il peut y avoir quelques incohérences.", "future_version_imported": "Le fichier importé viens d'une version postérieure de PleromaFE.", "older_version_imported": "Le fichier importé viens d'une version antérieure de PleromaFE.", "snapshot_source_mismatch": "Conflict de version : Probablement due à un retour arrière puis remise à jour de la version de PleromaFE, si vous avez charger le thème en utilisant une version antérieure vous voulez probablement utiliser la version antérieure, autrement utiliser la version postérieure.", @@ -409,7 +424,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" @@ -426,7 +447,7 @@ "filter_hint": { "always_drop_shadow": "Attention, cette ombre utilise toujours {0} quand le navigateur le supporte.", "drop_shadow_syntax": "{0} ne supporte pas le paramètre {1} et mot-clé {2}.", - "avatar_inset": "Veuillez noter que combiner a la fois les ombres internes et non-internes sur les avatars peut fournir des résultats innatendus avec la transparence des avatars.", + "avatar_inset": "Veuillez noter que combiner à la fois les ombres internes et non-internes sur les avatars peut fournir des résultats inattendus avec la transparence des avatars.", "spread_zero": "Les ombres avec une dispersion > 0 apparaitrons comme si ils étaient à zéro", "inset_classic": "L'ombre interne utilisera toujours {0}" }, @@ -481,15 +502,15 @@ }, "change_email": "Changer de courriel", "domain_mutes": "Domaines", - "pad_emoji": "Rajouter un espace autour de l'émoji après l’avoir choisit", + "pad_emoji": "Entourer les émoji d'espaces après leur sélections", "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", - "change_email_error": "Il y a eu un problème pour charger votre courriel.", + "allow_following_move": "Activer le suivit automatique à la migration des comptes", + "change_email_error": "Il y a eu un problème pour changer votre courriel.", "changed_email": "Courriel changé avec succès !", "discoverable": "Permettre de découvrir ce compte dans les résultats de recherche web et autres services", "emoji_reactions_on_timeline": "Montrer les émojis-réactions dans le flux", @@ -504,31 +525,75 @@ "accent": "Accent", "chatMessageRadius": "Message de chat", "bot": "Ce compte est un robot", - "import_mutes_from_a_csv_file": "Importer les masquages depuis un fichier CSV", + "import_mutes_from_a_csv_file": "Import de masquages depuis un fichier CSV", "mutes_imported": "Masquages importés ! Leur application peut prendre du temps.", "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 des flux", + "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 le fond du profil", + "reset_avatar": "Réinitialiser l'avatar", + "profile_fields": { + "value": "Contenu", + "name": "Nom du champ", + "add_field": "Ajouter un champ", + "label": "Champs du profil" + }, + "hide_media_previews": "Cacher la prévisualisation des pièces jointes", + "mutes_and_blocks": "Masquage et Blocages", + "setting_changed": "Préférence modifiée", + "more_settings": "Plus de préférences", + "sensitive_by_default": "Marquer les messages comme sensible par défaut", + "reply_visibility_self_short": "Uniquement les réponses à moi", + "reply_visibility_following_short": "Montrer les réponses à mes suivis", + "hide_wallpaper": "Cacher le fond d'écran", + "hide_all_muted_posts": "Cacher les messages masqués", + "word_filter": "Filtrage par mots", + "save": "Enregistrer les changements", + "file_export_import": { + "backup_settings_theme": "Sauvegarder les paramètres et le thème dans un fichier", + "errors": { + "invalid_file": "Le fichier sélectionné n'est pas un format supporté pour les sauvegarde Pleroma. Aucun changement n'a été fait.", + "file_too_new": "Version majeure incompatible. {fileMajor}, ce PleromaFE ({feMajor}) est trop ancien", + "file_too_old": "Version majeure incompatible : {fileMajor}, la version du fichier est trop vielle et n'est plus supportée (vers. min. {feMajor})", + "file_slightly_new": "La version mineure du fichier est différente, quelques paramètres on pût ne pas chargés" + }, + "backup_restore": "Sauvegarde des Paramètres", + "backup_settings": "Sauvegarder les paramètres dans un fichier", + "restore_settings": "Restaurer les paramètres depuis un fichier" + }, + "hide_shoutbox": "Cacher la shoutbox de l'instance", + "right_sidebar": "Afficher le paneau latéral à droite" }, "timeline": { "collapse": "Fermer", "conversation": "Conversation", "error_fetching": "Erreur en cherchant les mises à jour", - "load_older": "Afficher plus", + "load_older": "Afficher des status plus ancien", "no_retweet_hint": "Le message est marqué en abonnés-seulement ou direct et ne peut pas être partagé", "repeated": "a partagé", "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 flux : {0}", + "socket_broke": "Connexion temps-réel perdue : CloseEvent code {0}", + "socket_reconnected": "Connexion temps-réel établie" }, "status": { "favorites": "Favoris", "repeats": "Partages", "delete": "Supprimer statuts", - "pin": "Agraffer sur le profil", - "unpin": "Dégraffer du profil", + "pin": "Agrafer sur le profil", + "unpin": "Dégrafer du profil", "pinned": "Agraffé", "delete_confirm": "Voulez-vous vraiment supprimer ce statuts ?", "reply_to": "Réponse à ", @@ -536,7 +601,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", @@ -547,7 +624,6 @@ "follow": "Suivre", "follow_sent": "Demande envoyée !", "follow_progress": "Demande en cours…", - "follow_again": "Renvoyer la demande ?", "follow_unfollow": "Désabonner", "followees": "Suivis", "followers": "Vous suivent", @@ -591,10 +667,23 @@ "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", + "highlight": { + "disabled": "Sans mise-en-valeur", + "solid": "Fond uni", + "side": "Coté rayé", + "striped": "Fond rayé" + }, + "bot": "Robot", + "edit_profile": "Éditer le profil" }, "user_profile": { - "timeline_title": "Journal de l'utilisateurâ‹…ice", + "timeline_title": "Flux du compte", "profile_does_not_exist": "Désolé, ce profil n'existe pas.", "profile_loading_error": "Désolé, il y a eu une erreur au chargement du profil." }, @@ -619,54 +708,56 @@ "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", - "KiB": "KiO", - "MiB": "MiO", - "GiB": "GiO", - "TiB": "TiO" + "B": "o", + "KiB": "Ko", + "MiB": "Mo", + "GiB": "Go", + "TiB": "To" } }, "about": { "mrf": { "keyword": { - "reject": "Rejeté", - "replace": "Remplacer", - "keyword_policies": "Politiques par mot-clés", + "reject": "Rejette", + "replace": "Remplace", + "keyword_policies": "Filtrage par mots-clés", "ftl_removal": "Suppression du flux fédéré", "is_replaced_by": "→" }, "simple": { "simple_policies": "Politiques par instances", - "accept": "Accepter", - "accept_desc": "Cette instance accepte des messages seulement depuis ces instances :", - "reject": "Rejeter", + "accept": "Acceptées", + "accept_desc": "Cette instance accepte les messages seulement depuis ces instances :", + "reject": "Rejetées", "reject_desc": "Cette instance n'acceptera pas de message de ces instances :", "quarantine": "Quarantaine", - "quarantine_desc": "Cette instance enverras seulement des messages publics à ces instances :", - "ftl_removal_desc": "Cette instance supprime ces instance du flux fédéré :", - "media_removal": "Suppression multimédia", + "quarantine_desc": "Cette instance enverra seulement des messages publics à ces instances :", + "ftl_removal_desc": "Cette instance supprime les instance suivantes du flux fédéré :", + "media_removal": "Suppression des pièce-jointes", "media_removal_desc": "Cette instance supprime le contenu multimédia des instances suivantes :", "media_nsfw": "Force le contenu multimédia comme sensible", - "ftl_removal": "Suppression du flux fédéré", - "media_nsfw_desc": "Cette instance force le contenu multimédia comme sensible pour les messages des instances suivantes :" + "ftl_removal": "Supprimées du flux fédéré", + "media_nsfw_desc": "Cette instance force les pièce-jointes comme sensible pour les messages des instances suivantes :" }, "federation": "Fédération", - "mrf_policies": "Politiques MRF activées", + "mrf_policies": "Politiques MRF actives", "mrf_policies_desc": "Les politiques MRF modifient la fédération entre les instances. Les politiques suivantes sont activées :" }, "staff": "Staff" }, "domain_mute_card": { - "mute": "Muet", + "mute": "Masqué", "mute_progress": "Masquage…", "unmute": "Démasquer", "unmute_progress": "Démasquage…" @@ -683,7 +774,9 @@ "expires_in": "Fin du sondage dans {0}", "not_enough_options": "Trop peu d'options unique au sondage", "vote": "Voter", - "expired": "Sondage terminé il y a {0}" + "expired": "Sondage terminé il y a {0}", + "people_voted_count": "{count} voteur | {count} voteurs", + "votes_count": "{count} vote | {count} votes" }, "emoji": { "emoji": "Émoji", @@ -694,11 +787,11 @@ "load_all": "Charger tout les {emojiAmount} émojis", "load_all_hint": "{saneAmount} émojis chargé, charger tout les émojis peuvent causer des problèmes de performances.", "stickers": "Stickers", - "keep_open": "Garder le sélecteur ouvert" + "keep_open": "Garder ouvert" }, "remote_user_resolver": { "error": "Non trouvé.", - "searching_for": "Rechercher", + "searching_for": "Recherche pour", "remote_user_resolver": "Résolution de compte distant" }, "time": { @@ -759,5 +852,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/he.json b/src/i18n/he.json index 4b920536a650a19ea1a15e310879c2a9d92e22b7..b0c59a30e5d595a7c3bb9a5779682e071446153d 100644 --- a/src/i18n/he.json +++ b/src/i18n/he.json @@ -312,7 +312,6 @@ "follow": "עקוב", "follow_sent": "בקשה × ×©×œ×—×”!", "follow_progress": "מבקש…", - "follow_again": "שלח בקשה שוב?", "follow_unfollow": "בטל עקיבה", "followees": "× ×¢×§×‘×™×", "followers": "עוקבי×", diff --git a/src/i18n/id.json b/src/i18n/id.json new file mode 100644 index 0000000000000000000000000000000000000000..fd64b7aee08caa954dcdf4b764e1c6df8b088afe --- /dev/null +++ b/src/i18n/id.json @@ -0,0 +1,631 @@ +{ + "settings": { + "style": { + "preview": { + "link": "sebuah tautan yang kecil nan bagus", + "header": "Pratinjau", + "error": "Contoh kesalahan", + "button": "Tombol", + "input": "Baru saja mendarat di L.A.", + "faint_link": "manual berguna", + "fine_print": "Baca {0} kami untuk belajar sesuatu yang tak ada gunanya!", + "header_faint": "Ini baik-baik saja", + "checkbox": "Saya telah membaca sekilas syarat dan ketentuan" + }, + "advanced_colors": { + "alert_neutral": "Neutral", + "alert_warning": "Peringatan", + "alert_error": "Kesalahan", + "_tab_label": "Lanjutan", + "post": "Postingan/Bio pengguna", + "popover": "Tooltip, menu, popover", + "badge_notification": "Notifikasi", + "top_bar": "Bar atas", + "borders": "", + "buttons": "Tombol", + "wallpaper": "Latar belakang", + "panel_header": "Header panel", + "icons": "Ikon-ikon", + "disabled": "Dinonaktifkan" + }, + "common_colors": { + "main": "Warna umum", + "_tab_label": "Umum" + }, + "common": { + "contrast": { + "context": { + "text": "untuk teks", + "18pt": "Untuk teks besar (18pt+)" + } + }, + "color": "Warna" + }, + "switcher": { + "help": { + "upgraded_from_v2": "PleromaFE telah diperbarui, tema dapat terlihat sedikit berbeda dari apa yang Anda ingat.", + "future_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih baru.", + "older_version_imported": "Berkas yang Anda impor dibuat pada versi FE yang lebih lama.", + "fe_upgraded": "Mesin tema PleromaFE diperbarui setelah pembaruan versi." + }, + "use_source": "Versi baru", + "use_snapshot": "Versi lama", + "load_theme": "Muat tema" + }, + "fonts": { + "_tab_label": "Font", + "components": { + "interface": "Antarmuka", + "post": "Teks postingan" + }, + "family": "Nama font", + "size": "Ukuran (dalam px)", + "weight": "Berat (ketebalan)" + }, + "shadows": { + "components": { + "panel": "Panel", + "panelHeader": "Header panel" + } + } + }, + "notification_setting_privacy": "Privasi", + "notifications": "Notifikasi", + "values": { + "true": "ya", + "false": "tidak" + }, + "user_settings": "Pengaturan Pengguna", + "upload_a_photo": "Unggah foto", + "theme": "Tema", + "text": "Teks", + "settings": "Pengaturan", + "security_tab": "Keamanan", + "saving_ok": "Pengaturan disimpan", + "profile_tab": "Profil", + "profile_background": "Latar belakang profil", + "token": "Token", + "oauth_tokens": "Token OAuth", + "show_moderator_badge": "Tampilkan lencana \"Moderator\" di profil saya", + "show_admin_badge": "Tampilkan lencana \"Admin\" di profil saya", + "new_password": "Kata sandi baru", + "new_email": "Surel baru", + "name_bio": "Nama & bio", + "name": "Nama", + "profile_fields": { + "value": "Isi", + "name": "Label", + "label": "Metadata profil" + }, + "limited_availability": "Tidak tersedia di browser Anda", + "invalid_theme_imported": "Berkas yang dipilih bukan sebuah tema yang didukung Pleroma. Tidak ada perbuahan yang dibuat pada tema Anda.", + "interfaceLanguage": "Bahasa antarmuka", + "interface": "Antarmuka", + "instance_default_simple": "(bawaan)", + "instance_default": "(bawaan: {value})", + "general": "Umum", + "delete_account_error": "Ada masalah ketika menghapus akun Anda. Jika ini terus terjadi harap hubungi adminstrator instansi Anda.", + "delete_account_description": "Hapus data Anda secara permanen dan menonaktifkan akun Anda.", + "delete_account": "Hapus akun", + "data_import_export_tab": "Impor / ekspor data", + "current_password": "Kata sandi saat ini", + "confirm_new_password": "Konfirmasi kata sandi baru", + "version": { + "title": "Versi", + "backend_version": "Versi backend", + "frontend_version": "Versi frontend" + }, + "security": "Keamanan", + "changed_password": "Kata sandi berhasil diubah!", + "change_password_error": "Ada masalah ketika mengubah kata sandi Anda.", + "change_password": "Ubah kata sandi", + "changed_email": "Surel berhasil diubah!", + "change_email_error": "Ada masalah ketika mengubah surel Anda.", + "change_email": "Ubah surel", + "cRed": "Merah (Batal)", + "cBlue": "Biru (Balas, ikuti)", + "btnRadius": "Tombol", + "bot": "Ini adalah akun bot", + "block_export": "Ekspor blokiran", + "bio": "Bio", + "background": "Latar belakang", + "avatarRadius": "Avatar", + "avatar": "Avatar", + "attachments": "Lampiran", + "mfa": { + "scan": { + "title": "Pindai" + }, + "confirm_and_enable": "Konfirmasi & aktifkan OTP", + "setup_otp": "Siapkan OTP", + "otp": "OTP", + "recovery_codes_warning": "Tulis kode-kode nya atau simpan mereka di tempat yang aman - jika tidak Anda tidak akan melihat mereka lagi. Jika Anda tidak dapat mengakses aplikasi 2FA Anda dan kode pemulihan Anda hilang Anda tidak akan bisa mengakses akun Anda.", + "authentication_methods": "Metode otentikasi", + "recovery_codes": "Kode pemulihan.", + "warning_of_generate_new_codes": "Ketika Anda menghasilkan kode pemulihan baru, kode lama Anda berhenti bekerja.", + "generate_new_recovery_codes": "Hasilkan kode pemulihan baru", + "title": "Otentikasi Dua-faktor", + "waiting_a_recovery_codes": "Menerima kode cadangan…", + "verify": { + "desc": "Untuk mengaktifkan otentikasi dua-faktor, masukkan kode dari aplikasi dua-faktor Anda:" + } + }, + "app_name": "Nama aplikasi", + "save": "Simpan perubahan", + "valid_until": "Valid hingga", + "follow_import_error": "Terjadi kesalahan ketika mengimpor pengikut", + "emoji_reactions_on_timeline": "Tampilkan reaksi emoji pada linimasa", + "chatMessageRadius": "Pesan obrolan", + "cOrange": "Jingga (Favorit)", + "avatarAltRadius": "Avatar (notifikasi)", + "hide_shoutbox": "Sembunyikan kotak suara instansi", + "hide_followers_count_description": "Jangan tampilkan jumlah pengikut", + "hide_follows_count_description": "Jangan tampilkan jumlah mengikuti", + "hide_followers_description": "Jangan tampilkan siapa yang mengikuti saya", + "hide_follows_description": "Jangan tampilkan siapa yang saya ikuti", + "notification_visibility_emoji_reactions": "Reaksi", + "notification_visibility_follows": "Diikuti", + "notification_visibility_moves": "Pengguna Bermigrasi", + "notification_visibility_repeats": "Ulangan", + "notification_visibility_mentions": "Sebutan", + "notification_visibility_likes": "Favorit", + "notification_visibility": "Jenis notifikasi yang perlu ditampilkan", + "links": "Tautan", + "hide_user_stats": "Sembunyikan statistik pengguna (contoh. jumlah pengikut)", + "hide_post_stats": "Sembunyikan statistik postingan (contoh. jumlah favorit)", + "use_one_click_nsfw": "Buka lampiran NSFW hanya dengan satu klik", + "hide_wallpaper": "Sembunyikan latar belakang instansi", + "blocks_imported": "Blokiran diimpor! Pemrosesannya mungkin memakan sedikit waktu.", + "block_import_error": "Terjadi kesalahan ketika mengimpor blokiran", + "block_import": "Impor blokiran", + "block_export_button": "Ekspor blokiran Anda menjadi berkas csv", + "blocks_tab": "Blokiran", + "delete_account_instructions": "Ketik kata sandi Anda pada input di bawah untuk mengkonfirmasi penghapusan akun.", + "mutes_and_blocks": "Bisuan dan Blokiran", + "enter_current_password_to_confirm": "Masukkan kata sandi Anda saat ini untuk mengkonfirmasi identitas Anda", + "filtering": "Penyaringan", + "word_filter": "Penyaring kata", + "avatar_size_instruction": "Ukuran minimum gambar avatar yang disarankan adalah 150x150 piksel.", + "attachmentRadius": "Lampiran", + "cGreen": "Hijau (Retweet)", + "max_thumbnails": "Jumlah thumbnail maksimum per postingan", + "loop_video": "Ulang-ulang video", + "loop_video_silent_only": "Ulang-ulang video tanpa suara (seperti \"gif\" Mastodon)", + "pause_on_unfocused": "Jeda aliran ketika tab di dalam fokus", + "reply_visibility_following": "Hanya tampilkan balasan yang ditujukan kepada saya atau orang yang saya ikuti", + "reply_visibility_following_short": "Tampilkan balasan ke orang yang saya ikuti", + "saving_err": "Terjadi kesalahan ketika menyimpan pengaturan", + "search_user_to_block": "Cari siapa yang Anda ingin blokir", + "search_user_to_mute": "Cari siapa yang ingin Anda bisukan", + "set_new_avatar": "Tetapkan avatar baru", + "set_new_profile_background": "Tetapkan latar belakang profil baru", + "subject_line_behavior": "Salin subyek ketika membalas", + "subject_line_email": "Seperti surel: \"re: subyek\"", + "subject_line_mastodon": "Seperti mastodon: salin saja", + "subject_line_noop": "Jangan salin", + "useStreamingApiWarning": "(Tidak disarankan, eksperimental, diketahui dapat melewati postingan-postingan)", + "fun": "Seru", + "enable_web_push_notifications": "Aktifkan notifikasi push web", + "more_settings": "Lebih banyak pengaturan", + "reply_visibility_all": "Tampilkan semua balasan", + "reply_visibility_self": "Hanya tampilkan balasan yang ditujukan kepada saya", + "hide_muted_posts": "Sembunyikan postingan-postingan dari pengguna yang dibisukan", + "import_blocks_from_a_csv_file": "Impor blokiran dari berkas csv", + "domain_mutes": "Domain", + "composing": "Menulis", + "no_blocks": "Tidak ada yang diblokir", + "no_mutes": "Tidak ada yang dibisukan" + }, + "about": { + "mrf": { + "keyword": { + "reject": "Tolak", + "is_replaced_by": "→" + }, + "simple": { + "quarantine_desc": "Instansi ini hanya akan mengirim postingan publik ke instansi-instansi berikut:", + "quarantine": "Karantina", + "reject_desc": "Instansi ini tidak akan menerima pesan dari instansi-instansi berikut:", + "reject": "Tolak", + "accept_desc": "Instansi ini hanya menerima pesan dari instansi-instansi berikut:", + "accept": "Terima", + "media_removal": "Penghapusan Media", + "media_removal_desc": "Instansi ini menghapus media dari postingan yang berasal dari instansi-instansi berikut:" + }, + "federation": "Federasi", + "mrf_policies": "Kebijakan MRF yang diaktifkan" + }, + "staff": "Staf" + }, + "time": { + "day": "{0} hari", + "days": "{0} hari", + "day_short": "{0}h", + "days_short": "{0}h", + "hour": "{0} jam", + "hours": "{0} jam", + "hour_short": "{0}j", + "hours_short": "{0}j", + "in_future": "dalam {0}", + "in_past": "{0} yang lalu", + "minute": "{0} menit", + "minutes": "{0} menit", + "minute_short": "{0}m", + "minutes_short": "{0}m", + "month": "{0} bulan", + "months": "{0} bulan", + "month_short": "{0}b", + "months_short": "{0}b", + "now": "baru saja", + "now_short": "sekarang", + "second": "{0} detik", + "seconds": "{0} detik", + "second_short": "{0}d", + "seconds_short": "{0}d", + "week": "{0} pekan", + "weeks": "{0} pekan", + "week_short": "{0}p", + "weeks_short": "{0}p", + "year": "{0} tahun", + "years": "{0} tahun", + "year_short": "{0}t", + "years_short": "{0}t" + }, + "timeline": { + "conversation": "Percakapan", + "error": "Terjadi kesalahan memuat linimasa: {0}", + "no_retweet_hint": "Postingan ditandai sebagai hanya-pengikut atau langsung dan tidak dapat diulang", + "repeated": "diulangi", + "reload": "Muat ulang", + "no_more_statuses": "Tidak ada status lagi", + "no_statuses": "Tidak ada status" + }, + "status": { + "favorites": "Favorit", + "repeats": "Ulangan", + "delete": "Hapus status", + "pin": "Sematkan di profil", + "unpin": "Berhenti menyematkan dari profil", + "pinned": "Disematkan", + "delete_confirm": "Apakah Anda benar-benar ingin menghapus status ini?", + "reply_to": "Balas ke", + "replies_list": "Balasan:", + "mute_conversation": "Bisukan percakapan", + "unmute_conversation": "Berhenti membisikan percakapan", + "status_unavailable": "Status tidak tersedia", + "thread_muted_and_words": ", memiliki kata:", + "hide_content": "", + "show_content": "", + "status_deleted": "Postingan ini telah dihapus", + "nsfw": "NSFW" + }, + "user_card": { + "block": "Blokir", + "blocked": "Diblokir!", + "deny": "Tolak", + "edit_profile": "Sunting profil", + "favorites": "Favorit", + "follow": "Ikuti", + "follow_sent": "Permintaan dikirim!", + "follow_progress": "Meminta…", + "mute": "Bisukan", + "muted": "Dibisukan", + "per_day": "per hari", + "report": "Laporkan", + "statuses": "Status", + "unblock": "Berhenti memblokir", + "block_progress": "Memblokir…", + "unmute": "Berhenti membisukan", + "mute_progress": "Membisukan…", + "hide_repeats": "Sembunyikan ulangan", + "show_repeats": "Tampilkan ulangan", + "bot": "Bot", + "admin_menu": { + "moderation": "Moderasi", + "activate_account": "Aktifkan akun", + "deactivate_account": "Nonaktifkan akun", + "delete_account": "Hapus akun", + "force_nsfw": "Tandai semua postingan sebagai NSFW", + "strip_media": "Hapus media dari postingan-postingan", + "delete_user": "Hapus pengguna", + "delete_user_confirmation": "Apakah Anda benar-benar yakin? Tindakan ini tidak dapat dibatalkan." + }, + "follow_unfollow": "Berhenti mengikuti", + "followees": "Mengikuti", + "followers": "Pengikut", + "following": "Diikuti!", + "follows_you": "Mengikuti Anda!", + "hidden": "Disembunyikan", + "its_you": "Ini Anda!", + "media": "Media", + "mention": "Sebut", + "message": "Kirimkan pesan" + }, + "user_profile": { + "timeline_title": "Linimasa pengguna", + "profile_does_not_exist": "Maaf, profil ini tidak ada.", + "profile_loading_error": "Maaf, terjadi kesalahan ketika memuat profil ini." + }, + "user_reporting": { + "title": "Melaporkan {0}", + "add_comment_description": "Laporan ini akan dikirim ke moderator instansi Anda. Anda dapat menyediakan penjelasan mengapa Anda melaporkan akun ini di bawah:", + "additional_comments": "Komentar tambahan", + "forward_description": "Akun ini berada di server lain. Kirim salinan dari laporannya juga?", + "submit": "Kirim", + "generic_error": "Sebuah kesalahan terjadi ketika memproses permintaan Anda." + }, + "notifications": { + "favorited_you": "memfavoritkan status Anda", + "reacted_with": "bereaksi dengan {0}", + "no_more_notifications": "Tidak ada notifikasi lagi", + "repeated_you": "mengulangi status Anda", + "read": "Dibaca!", + "notifications": "Notifikasi", + "follow_request": "ingin mengikuti Anda", + "followed_you": "mengikuti Anda", + "error": "Terjadi kesalahan ketika memuat notifikasi: {0}", + "migrated_to": "bermigrasi ke", + "load_older": "Muat notifikasi yang lebih lama", + "broken_favorite": "Status tak diketahui, mencarinya…" + }, + "who_to_follow": { + "more": "Lebih banyak" + }, + "tool_tip": { + "media_upload": "Unggah media", + "repeat": "Ulangi", + "reply": "Balas", + "favorite": "Favorit", + "add_reaction": "Tambahkan Reaksi", + "user_settings": "Pengaturan Pengguna" + }, + "upload": { + "error": { + "base": "Pengunggahan gagal.", + "message": "Pengunggahan gagal: {0}", + "file_too_big": "Berkas terlalu besar [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Coba lagi nanti" + }, + "file_size_units": { + "B": "B", + "KiB": "KiB", + "MiB": "MiB", + "GiB": "GiB", + "TiB": "TiB" + } + }, + "search": { + "people": "Orang", + "hashtags": "Tagar", + "person_talking": "{count} orang berbicara", + "people_talking": "{count} orang berbicara", + "no_results": "Tidak ada hasil" + }, + "password_reset": { + "forgot_password": "Lupa kata sandi?", + "placeholder": "Surel atau nama pengguna Anda", + "return_home": "Kembali ke halaman beranda", + "too_many_requests": "Anda telah mencapai batas percobaan, coba lagi nanti.", + "instruction": "Masukkan surel atau nama pengguna Anda. Kami akan mengirimkan Anda tautan untuk mengatur ulang kata sandi.", + "password_reset": "Pengatur-ulangan kata sandi", + "password_reset_disabled": "Pengatur-ulangan kata sandi dinonaktifkan. Hubungi administrator instansi Anda.", + "password_reset_required": "Anda harus mengatur ulang kata sandi Anda untuk masuk.", + "password_reset_required_but_mailer_is_disabled": "Anda harus mengatur ulang kata sandi, tetapi pengatur-ulangan kata sandi dinonaktifkan. Silakan hubungi administrator instansi Anda." + }, + "chats": { + "you": "Anda:", + "message_user": "Kirim Pesan ke {nickname}", + "delete": "Hapus", + "chats": "Obrolan", + "new": "Obrolan Baru", + "empty_message_error": "Tidak dapat memposting pesan yang kosong", + "more": "Lebih banyak", + "delete_confirm": "Apakah Anda benar-benar ingin menghapus pesan ini?", + "error_loading_chat": "Sesuatu yang salah terjadi ketika memuat obrolan.", + "error_sending_message": "Sesuatu yang salah terjadi ketika mengirim pesan.", + "empty_chat_list_placeholder": "Anda belum memiliki obrolan. Buat sbeuah obrolan baru!" + }, + "file_type": { + "audio": "Audio", + "video": "Video", + "image": "Gambar", + "file": "Berkas" + }, + "registration": { + "bio_placeholder": "contoh.\nHai, aku Lain.\nAku seorang putri anime yang tinggal di pinggiran kota Jepang. Kamu mungkin mengenal aku dari Wired.", + "validations": { + "password_confirmation_required": "tidak boleh kosong", + "password_required": "tidak boleh kosong", + "email_required": "tidak boleh kosong", + "fullname_required": "tidak boleh kosong", + "username_required": "tidak boleh kosong" + }, + "register": "Daftar", + "fullname_placeholder": "contoh. Lain Iwakura", + "username_placeholder": "contoh. lain", + "new_captcha": "Klik gambarnya untuk mendapatkan captcha baru", + "captcha": "CAPTCHA", + "token": "Token undangan", + "password_confirm": "Konfirmasi kata sandi", + "email": "Surel", + "bio": "Bio", + "reason_placeholder": "Instansi ini menerima pendaftaran secara manual.\nBeritahu administrasinya mengapa Anda ingin mendaftar.", + "reason": "Alasan mendaftar", + "registration": "Pendaftaran" + }, + "post_status": { + "preview_empty": "Kosong", + "default": "Baru saja mendarat di L.A.", + "content_warning": "Subyek (opsional)", + "content_type": { + "text/bbcode": "BBCode", + "text/markdown": "Markdown", + "text/html": "HTML", + "text/plain": "Teks biasa" + }, + "media_description": "Keterangan media", + "attachments_sensitive": "Tandai lampiran sebagai sensitif", + "scope": { + "public": "Publik - posting ke linimasa publik", + "private": "Hanya-pengikut - posting hanya kepada pengikut", + "direct": "Langsung - posting hanya kepada pengguna yang disebut" + }, + "preview": "Pratinjau", + "post": "Posting", + "posting": "Memposting", + "direct_warning_to_first_only": "Postingan ini akan terlihat oleh pengguna yang disebutkan di awal pesan.", + "direct_warning_to_all": "Postingan ini akan terlihat oleh pengguna yang disebutkan.", + "scope_notice": { + "private": "Postingan ini akan terlihat hanya oleh pengikut Anda", + "public": "Postingan ini akan terlihat oleh siapa saja" + }, + "media_description_error": "Gagal memperbarui media, coba lagi", + "empty_status_error": "Tidak dapat memposting status kosong tanpa berkas", + "account_not_locked_warning_link": "terkunci", + "account_not_locked_warning": "Akun Anda tidak {0}. Siapapun dapat mengikuti Anda untuk melihat postingan hanya-pengikut Anda.", + "new_status": "Posting status baru" + }, + "general": { + "apply": "Terapkan", + "flash_fail": "Gagal memuat konten flash, lihat console untuk keterangan.", + "flash_security": "Harap ingat ini dapat menjadi berbahaya karena konten Flash masih termasuk arbitrary code.", + "flash_content": "Klik untuk menampilkan konten Flash menggunakan Ruffle (Eksperimental, mungkin tidak bekerja).", + "role": { + "moderator": "Moderator", + "admin": "Admin" + }, + "peek": "Intip", + "close": "Tutup", + "verify": "Verifikasi", + "confirm": "Konfirmasi", + "enable": "Aktifkan", + "disable": "Nonaktifkan", + "cancel": "Batal", + "show_less": "Tampilkan lebih sedikit", + "show_more": "Tampilkan lebih banyak", + "optional": "opsional", + "retry": "Coba lagi", + "error_retry": "Harap coba lagi", + "generic_error": "Terjadi kesalahan", + "loading": "Memuat…", + "more": "Lebih banyak", + "submit": "Kirim" + }, + "remote_user_resolver": { + "error": "Tidak ditemukan." + }, + "emoji": { + "load_all": "Memuat semua {emojiAmount} emoji", + "load_all_hint": "Memuat {saneAmount} emoji pertama, memuat semua emoji dapat menyebabkan masalah performa.", + "unicode": "Emoji unicode", + "add_emoji": "Sisipkan emoji", + "search_emoji": "Cari emoji", + "emoji": "Emoji", + "stickers": "Stiker", + "keep_open": "Tetap buka pemilih", + "custom": "Emoji kustom" + }, + "polls": { + "expired": "Japat berakhir {0} yang lalu", + "expires_in": "Japat berakhir dalam {0}", + "expiry": "Usia japat", + "type": "Jenis japat", + "vote": "Pilih", + "votes_count": "{count} suara | {count} suara", + "people_voted_count": "{count} orang memilih | {count} orang memilih", + "votes": "suara", + "option": "Opsi", + "add_option": "Tambahkan opsi", + "add_poll": "Tambahkan japat", + "not_enough_options": "Terlalu sedikit opsi yang unik pada japat" + }, + "nav": { + "preferences": "Preferensi", + "search": "Cari", + "user_search": "Pencarian Pengguna", + "home_timeline": "Linimasa beranda", + "timeline": "Linimasa", + "public_tl": "Linimasa publik", + "interactions": "Interaksi", + "mentions": "Sebutan", + "back": "Kembali", + "administration": "Administrasi", + "about": "Tentang", + "timelines": "Linimasa", + "chats": "Obrolan", + "dms": "Pesan langsung", + "friend_requests": "Ingin mengikuti" + }, + "media_modal": { + "next": "Selanjutnya", + "previous": "Sebelum" + }, + "login": { + "recovery_code": "Kode pemulihan", + "enter_recovery_code": "Masukkan kode pemulihan", + "authentication_code": "Kode otentikasi", + "hint": "Masuk untuk ikut berdiskusi", + "username": "Nama pengguna", + "register": "Daftar", + "placeholder": "contoh: lain", + "password": "Kata sandi", + "logout": "Keluar", + "description": "Masuk dengan OAuth", + "login": "Masuk", + "heading": { + "totp": "Otentikasi dua-faktor" + }, + "enter_two_factor_code": "Masukkan kode dua-faktor" + }, + "importer": { + "error": "Terjadi kesalahan ketika mnengimpor berkas ini.", + "success": "Berhasil mengimpor.", + "submit": "Kirim" + }, + "image_cropper": { + "cancel": "Batal", + "save_without_cropping": "Simpan tanpa memotong", + "save": "Simpan", + "crop_picture": "Potong gambar" + }, + "finder": { + "find_user": "Cari pengguna", + "error_fetching_user": "Terjadi kesalahan ketika memuat pengguna" + }, + "features_panel": { + "title": "Fitur-fitur", + "text_limit": "Batas teks", + "gopher": "Gopher", + "pleroma_chat_messages": "Pleroma Obrolan", + "chat": "Obrolan", + "upload_limit": "Batas unggahan" + }, + "exporter": { + "processing": "Memproses, Anda akan segera diminta untuk mengunduh berkas Anda", + "export": "Ekspor" + }, + "domain_mute_card": { + "unmute": "Berhenti membisukan", + "mute_progress": "Membisukan…", + "mute": "Bisukan", + "unmute_progress": "Memberhentikan pembisuan…" + }, + "display_date": { + "today": "Hari Ini" + }, + "selectable_list": { + "select_all": "Pilih semua" + }, + "interactions": { + "moves": "Pengguna yang bermigrasi", + "follows": "Pengikut baru", + "favs_repeats": "Ulangan dan favorit", + "load_older": "Muat interaksi yang lebih tua" + }, + "errors": { + "storage_unavailable": "Pleroma tidak dapat mengakses penyimpanan browser. Login Anda atau pengaturan lokal Anda tidak akan tersimpan dan masalah yang tidak terduga dapat terjadi. Coba mengaktifkan kuki." + }, + "shoutbox": { + "title": "Kotak Suara" + } +} diff --git a/src/i18n/it.json b/src/i18n/it.json index b7d21e7ed9eb1bfb080763e973abaf2bc8bb6723..a1ec37a28e78b48b7f2abb943a1410e4a61e23da 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -17,13 +17,20 @@ "close": "Chiudi", "retry": "Riprova", "error_retry": "Per favore, riprova", - "loading": "Carico…" + "loading": "Carico…", + "role": { + "moderator": "Moderatore", + "admin": "Amministratore" + }, + "flash_fail": "Contenuto Flash non caricato, vedi console del browser.", + "flash_content": "Mostra contenuto Flash tramite Ruffle (funzione in prova).", + "flash_security": "Può essere pericoloso perché i contenuti in Flash sono eseguibili." }, "nav": { "mentions": "Menzioni", "public_tl": "Sequenza pubblica", "timeline": "Sequenza personale", - "twkn": "Sequenza globale", + "twkn": "Sequenza federale", "chat": "Chat della stanza", "friend_requests": "Vogliono seguirti", "about": "Informazioni", @@ -37,14 +44,15 @@ "preferences": "Preferenze", "bookmarks": "Segnalibri", "chats": "Conversazioni", - "timelines": "Sequenze" + "timelines": "Sequenze", + "home_timeline": "Sequenza personale" }, "notifications": { "followed_you": "ti segue", "notifications": "Notifiche", "read": "Letto!", "broken_favorite": "Stato sconosciuto, lo sto cercando…", - "favorited_you": "gradisce il tuo messaggio", + "favorited_you": "ha gradito", "load_older": "Carica notifiche precedenti", "repeated_you": "ha condiviso il tuo messaggio", "follow_request": "vuole seguirti", @@ -60,17 +68,17 @@ "current_avatar": "La tua icona attuale", "current_profile_banner": "Il tuo stendardo attuale", "filtering": "Filtri", - "filtering_explanation": "Tutti i post contenenti queste parole saranno silenziati, una per riga", + "filtering_explanation": "Tutti i messaggi contenenti queste parole saranno silenziati, una per riga", "hide_attachments_in_convo": "Nascondi gli allegati presenti nelle conversazioni", "hide_attachments_in_tl": "Nascondi gli allegati presenti nelle sequenze", "name": "Nome", "name_bio": "Nome ed introduzione", "nsfw_clickthrough": "Fai click per visualizzare gli allegati offuscati", - "profile_background": "Sfondo della tua pagina", - "profile_banner": "Stendardo del tuo profilo", + "profile_background": "Sfondo del tuo profilo", + "profile_banner": "Gonfalone del tuo profilo", "set_new_avatar": "Scegli una nuova icona", - "set_new_profile_background": "Scegli un nuovo sfondo per la tua pagina", - "set_new_profile_banner": "Scegli un nuovo stendardo per il tuo profilo", + "set_new_profile_background": "Scegli un nuovo sfondo", + "set_new_profile_banner": "Scegli un nuovo gonfalone", "settings": "Impostazioni", "theme": "Tema", "user_settings": "Impostazioni Utente", @@ -79,9 +87,9 @@ "avatarRadius": "Icone utente", "background": "Sfondo", "btnRadius": "Pulsanti", - "cBlue": "Blu (risposte, seguire)", + "cBlue": "Blu (rispondi, segui)", "cGreen": "Verde (ripeti)", - "cOrange": "Arancione (gradire)", + "cOrange": "Arancione (gradisci)", "cRed": "Rosso (annulla)", "change_password": "Cambia password", "change_password_error": "C'è stato un problema durante il cambiamento della password.", @@ -94,7 +102,7 @@ "delete_account": "Elimina profilo", "delete_account_description": "Elimina definitivamente i tuoi dati e disattiva il tuo profilo.", "delete_account_error": "C'è stato un problema durante l'eliminazione del tuo profilo. Se il problema persiste contatta l'amministratore della tua stanza.", - "delete_account_instructions": "Digita la tua password nel campo sottostante per confermare l'eliminazione del tuo profilo.", + "delete_account_instructions": "Digita la tua password nel campo sottostante per eliminare il tuo profilo.", "export_theme": "Salva impostazioni", "follow_export": "Esporta la lista di chi segui", "follow_export_button": "Esporta la lista di chi segui in un file CSV", @@ -105,7 +113,7 @@ "foreground": "Primo piano", "general": "Generale", "hide_post_stats": "Nascondi statistiche dei messaggi (es. il numero di preferenze)", - "hide_user_stats": "Nascondi statistiche dell'utente (es. il numero dei tuoi seguaci)", + "hide_user_stats": "Nascondi statistiche dell'utente (es. il numero di seguaci)", "import_followers_from_a_csv_file": "Importa una lista di chi segui da un file CSV", "import_theme": "Carica impostazioni", "inputRadius": "Campi di testo", @@ -114,12 +122,12 @@ "invalid_theme_imported": "Il file selezionato non è un tema supportato da Pleroma. Il tuo tema non è stato modificato.", "limited_availability": "Non disponibile nel tuo browser", "links": "Collegamenti", - "lock_account_description": "Limita il tuo account solo a seguaci approvati", + "lock_account_description": "Vaglia manualmente i nuovi seguaci", "loop_video": "Riproduci video in ciclo continuo", - "loop_video_silent_only": "Riproduci solo video senza audio in ciclo continuo (es. le \"gif\" di Mastodon)", + "loop_video_silent_only": "Riproduci solo video muti in ciclo continuo (es. le \"gif\" di Mastodon)", "new_password": "Nuova password", "notification_visibility": "Tipi di notifiche da mostrare", - "notification_visibility_follows": "Nuove persone ti seguono", + "notification_visibility_follows": "Nuovi seguaci", "notification_visibility_likes": "Preferiti", "notification_visibility_mentions": "Menzioni", "notification_visibility_repeats": "Condivisioni", @@ -134,7 +142,7 @@ "presets": "Valori predefiniti", "profile_tab": "Profilo", "radii_help": "Imposta il raggio degli angoli (in pixel)", - "replies_in_timeline": "Risposte nella sequenza personale", + "replies_in_timeline": "Risposte nelle sequenze", "reply_visibility_all": "Mostra tutte le risposte", "reply_visibility_following": "Mostra solo le risposte rivolte a me o agli utenti che seguo", "reply_visibility_self": "Mostra solo risposte rivolte a me", @@ -144,7 +152,7 @@ "stop_gifs": "Riproduci GIF al passaggio del cursore", "streaming": "Mostra automaticamente i nuovi messaggi quando sei in cima alla pagina", "text": "Testo", - "theme_help": "Usa codici colore esadecimali (#rrggbb) per personalizzare il tuo schema di colori.", + "theme_help": "Usa colori esadecimali (#rrggbb) per personalizzare il tuo schema di colori.", "tooltipRadius": "Suggerimenti/avvisi", "values": { "false": "no", @@ -152,7 +160,7 @@ }, "avatar_size_instruction": "La taglia minima per l'icona personale è 150x150 pixel.", "domain_mutes": "Domini", - "discoverable": "Permetti la scoperta di questo profilo da servizi di ricerca ed altro", + "discoverable": "Permetti la scoperta di questo profilo a servizi di ricerca ed altro", "composing": "Composizione", "changed_email": "Email cambiata con successo!", "change_email_error": "C'è stato un problema nel cambiare la tua email.", @@ -163,18 +171,18 @@ "block_import": "Importa blocchi", "block_export_button": "Esporta i tuoi blocchi in un file CSV", "block_export": "Esporta blocchi", - "allow_following_move": "Consenti", + "allow_following_move": "Consenti l'iscrizione automatica ai profili traslocati", "mfa": { "verify": { "desc": "Per abilitare l'autenticazione bifattoriale, inserisci il codice fornito dalla tua applicazione:" }, "scan": { "secret_code": "Codice", - "desc": "Con la tua applicazione bifattoriale, acquisisci questo QR o inserisci il codice manualmente:", + "desc": "Con la tua applicazione bifattoriale, acquisisci il QR o inserisci il codice:", "title": "Acquisisci" }, "authentication_methods": "Metodi di accesso", - "recovery_codes_warning": "Appuntati i codici o salvali in un posto sicuro, altrimenti rischi di non rivederli mai più. Se perderai l'accesso sia alla tua applicazione bifattoriale che ai codici di recupero non potrai più accedere al tuo profilo.", + "recovery_codes_warning": "Metti i codici al sicuro, perché non potrai più visualizzarli. Se perderai l'accesso sia alla tua applicazione bifattoriale che ai codici di recupero non potrai più accedere al tuo profilo.", "waiting_a_recovery_codes": "Ricevo codici di recupero…", "recovery_codes": "Codici di recupero.", "warning_of_generate_new_codes": "Alla generazione di nuovi codici di recupero, quelli vecchi saranno disattivati.", @@ -193,14 +201,14 @@ "help": { "older_version_imported": "Il tema importato è stato creato per una versione precedente dell'interfaccia.", "future_version_imported": "Il tema importato è stato creato per una versione più recente dell'interfaccia.", - "v2_imported": "Il tema importato è stato creato per una vecchia interfaccia. Non tutto potrebbe essere come prima.", - "upgraded_from_v2": "L'interfaccia è stata aggiornata, il tema potrebbe essere diverso da come lo intendevi.", + "v2_imported": "Il tema importato è stato creato per una vecchia interfaccia. Non tutto potrebbe essere come inteso.", + "upgraded_from_v2": "L'interfaccia è stata aggiornata, il tema potrebbe essere diverso da come lo ricordi.", "migration_snapshot_ok": "Ho caricato l'anteprima del tema. Puoi provare a caricarne i contenuti.", "fe_downgraded": "L'interfaccia è stata portata ad una versione precedente.", "fe_upgraded": "Lo schema dei temi è stato aggiornato insieme all'interfaccia.", "snapshot_missing": "Il tema non è provvisto di anteprima, quindi potrebbe essere diverso da come appare.", "snapshot_present": "Tutti i valori sono sostituiti dall'anteprima del tema. Puoi invece caricare i suoi contenuti.", - "snapshot_source_mismatch": "Conflitto di versione: probabilmente l'interfaccia è stata portata ad una versione precedente e poi aggiornata di nuovo. Se hai modificato il tema con una versione precedente dell'interfaccia, usa la vecchia versione del tema, altrimenti puoi usare la nuova.", + "snapshot_source_mismatch": "Conflitto di versione: probabilmente l'interfaccia è stata portata indietro e poi aggiornata di nuovo. Se hai modificato il tema con una vecchia versione usa il tema precedente, altrimenti puoi usare il nuovo.", "migration_napshot_gone": "Anteprima del tema non trovata, non tutto potrebbe essere come ricordi." }, "use_source": "Nuova versione", @@ -223,7 +231,7 @@ "contrast": { "context": { "text": "per il testo", - "18pt": "per il testo grande (oltre 17pt)" + "18pt": "per il testo oltre 17pt" }, "level": { "bad": "non soddisfa le linee guida di alcun livello", @@ -246,7 +254,7 @@ "selectedMenu": "Voce menù selezionata", "selectedPost": "Messaggio selezionato", "pressed": "Premuto", - "highlight": "Elementi evidenziati", + "highlight": "Elementi in risalto", "icons": "Icone", "poll": "Grafico sondaggi", "underlay": "Sottostante", @@ -308,8 +316,8 @@ "fonts": { "_tab_label": "Font", "custom": "Personalizzato", - "weight": "Peso (grassettatura)", - "size": "Dimensione (in pixel)", + "weight": "Grassettatura", + "size": "Dimensione in pixel", "family": "Nome font", "components": { "postCode": "Font a spaziatura fissa incluso in un messaggio", @@ -336,15 +344,15 @@ }, "enable_web_push_notifications": "Abilita notifiche web push", "fun": "Divertimento", - "notification_mutes": "Per non ricevere notifiche da uno specifico utente, zittiscilo.", + "notification_mutes": "Per non ricevere notifiche da uno specifico utente, silenzialo.", "notification_setting_privacy_option": "Nascondi mittente e contenuti delle notifiche push", "notification_setting_privacy": "Privacy", "notification_setting_filters": "Filtri", "notifications": "Notifiche", "greentext": "Frecce da meme", "upload_a_photo": "Carica un'immagine", - "type_domains_to_mute": "Cerca domini da zittire", - "theme_help_v2_2": "Le icone dietro alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se si usano delle trasparenze, questi indicatori mostrano il peggior caso possibile.", + "type_domains_to_mute": "Cerca domini da silenziare", + "theme_help_v2_2": "Le icone vicino alcuni elementi sono indicatori del contrasto fra testo e sfondo, passaci sopra col puntatore per ulteriori informazioni. Se usani trasparenze, questi indicatori mostrano il peggior caso possibile.", "theme_help_v2_1": "Puoi anche forzare colore ed opacità di alcuni elementi selezionando la casella. Usa il pulsante \"Azzera\" per azzerare tutte le forzature.", "useStreamingApiWarning": "(Sconsigliato, sperimentale, può saltare messaggi)", "useStreamingApi": "Ricevi messaggi e notifiche in tempo reale", @@ -357,23 +365,23 @@ "subject_input_always_show": "Mostra sempre il campo Oggetto", "minimal_scopes_mode": "Riduci opzioni di visibilità ", "scope_copy": "Risposte ereditano la visibilità (messaggi privati lo fanno sempre)", - "search_user_to_mute": "Cerca utente da zittire", + "search_user_to_mute": "Cerca utente da silenziare", "search_user_to_block": "Cerca utente da bloccare", "autohide_floating_post_button": "Nascondi automaticamente il pulsante di composizione (mobile)", - "show_moderator_badge": "Mostra l'insegna di moderatore sulla mia pagina", - "show_admin_badge": "Mostra l'insegna di amministratore sulla mia pagina", + "show_moderator_badge": "Mostra l'insegna di moderatore sul mio profilo", + "show_admin_badge": "Mostra l'insegna di amministratore sul mio profilo", "hide_followers_count_description": "Non mostrare quanti seguaci ho", "hide_follows_count_description": "Non mostrare quanti utenti seguo", "hide_followers_description": "Non mostrare i miei seguaci", "hide_follows_description": "Non mostrare chi seguo", - "no_mutes": "Nessun utente zittito", + "no_mutes": "Nessun utente silenziato", "no_blocks": "Nessun utente bloccato", "notification_visibility_emoji_reactions": "Reazioni", "notification_visibility_moves": "Migrazioni utenti", "new_email": "Nuova email", "use_contain_fit": "Non ritagliare le anteprime degli allegati", "play_videos_in_modal": "Riproduci video in un riquadro a sbalzo", - "mutes_tab": "Zittiti", + "mutes_tab": "Silenziati", "interface": "Interfaccia", "instance_default_simple": "(predefinito)", "checkboxRadius": "Caselle di selezione", @@ -383,59 +391,87 @@ "preload_images": "Precarica immagini", "hide_isp": "Nascondi pannello della stanza", "max_thumbnails": "Numero massimo di anteprime per messaggio", - "hide_muted_posts": "Nascondi messaggi degli utenti zilenziati", + "hide_muted_posts": "Nascondi messaggi degli utenti silenziati", "accent": "Accento", - "emoji_reactions_on_timeline": "Mostra emoji di reazione sulle sequenze", + "emoji_reactions_on_timeline": "Mostra reazioni nelle sequenze", "pad_emoji": "Affianca spazi agli emoji inseriti tramite selettore", "notification_blocks": "Bloccando un utente non riceverai più le sue notifiche né lo seguirai più.", - "mutes_and_blocks": "Zittiti e bloccati", + "mutes_and_blocks": "Silenziati e bloccati", "profile_fields": { "value": "Contenuto", - "name": "Etichetta", + "name": "Descrizione", "add_field": "Aggiungi campo", "label": "Metadati profilo" }, - "bot": "Questo profilo è di un robot", + "bot": "Questo è un robot", "version": { "frontend_version": "Versione interfaccia", "backend_version": "Versione backend", "title": "Versione" }, "reset_avatar": "Azzera icona", - "reset_profile_background": "Azzera sfondo profilo", - "reset_profile_banner": "Azzera stendardo profilo", + "reset_profile_background": "Azzera sfondo", + "reset_profile_banner": "Azzera gonfalone", "reset_avatar_confirm": "Vuoi veramente azzerare l'icona?", - "reset_banner_confirm": "Vuoi veramente azzerare lo stendardo?", + "reset_banner_confirm": "Vuoi veramente azzerare il gonfalone?", "reset_background_confirm": "Vuoi veramente azzerare lo sfondo?", "chatMessageRadius": "Messaggi istantanei", "notification_setting_hide_notification_contents": "Nascondi mittente e contenuti delle notifiche push", "notification_setting_block_from_strangers": "Blocca notifiche da utenti che non segui", "virtual_scrolling": "Velocizza l'elaborazione delle sequenze", "import_mutes_from_a_csv_file": "Importa silenziati da un file CSV", - "mutes_imported": "Silenziati importati! Saranno elaborati a breve.", + "mutes_imported": "Silenziati importati! Elaborazione in corso.", "mute_import_error": "Errore nell'importazione", - "mute_import": "Importa silenziati", - "mute_export_button": "Esporta la tua lista di silenziati in un file CSV", + "mute_import": "Carica silenziati", + "mute_export_button": "Esporta i 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", + "more_settings": "Altre impostazioni", + "sensitive_by_default": "Tutti i miei messaggi sono scabrosi", + "reply_visibility_self_short": "Vedi solo risposte a te", + "reply_visibility_following_short": "Vedi risposte a messaggi di altri", + "hide_all_muted_posts": "Nascondi messaggi silenziati", + "hide_media_previews": "Nascondi anteprime", + "word_filter": "Parole filtrate", + "save": "Salva modifiche", + "file_export_import": { + "errors": { + "file_slightly_new": "Versione minore diversa, qualcosa potrebbe non combaciare.", + "file_too_old": "Versione troppo vecchia: {fileMajor}. Questa versione dell'interfaccia ({feMajor}) non supporta il file.", + "file_too_new": "Versione troppo recente: {fileMajor}. Questa versione dell'interfaccia ({feMajor}) non supporta il file.", + "invalid_file": "Il file selezionato non è un archivio supportato. Nessuna modifica è stata apportata." + }, + "restore_settings": "Carica impostazioni sul server", + "backup_settings_theme": "Archivia impostazioni e tema localmente", + "backup_settings": "Archivia impostazioni localmente", + "backup_restore": "Archiviazione impostazioni" + }, + "right_sidebar": "Mostra barra laterale a destra", + "hide_shoutbox": "Nascondi muro dei graffiti", + "mentions_new_style": "Menzioni abbreviate", + "mentions_new_place": "Segrega le menzioni", + "always_show_post_button": "Non nascondere il pulsante di composizione" }, "timeline": { "error_fetching": "Errore nell'aggiornamento", - "load_older": "Carica messaggi più vecchi", + "load_older": "Carica messaggi precedenti", "show_new": "Mostra nuovi", "up_to_date": "Aggiornato", - "collapse": "Riduci", + "collapse": "Ripiega", "conversation": "Conversazione", "no_retweet_hint": "Il messaggio è diretto o solo per seguaci e non può essere condiviso", - "repeated": "condiviso", + "repeated": "ha condiviso", "no_statuses": "Nessun messaggio", "no_more_statuses": "Fine dei messaggi", "reload": "Ricarica", - "error": "Errore nel caricare la sequenza: {0}" + "error": "Errore nel caricare la sequenza: {0}", + "socket_broke": "Connessione tempo reale interrotta: codice {0}", + "socket_reconnected": "Connesso in tempo reale" }, "user_card": { "follow": "Segui", - "followees": "Chi stai seguendo", + "followees": "Segue", "followers": "Seguaci", "following": "Seguìto!", "follows_you": "Ti segue!", @@ -449,13 +485,13 @@ "deny": "Nega", "remote_follow": "Segui da remoto", "admin_menu": { - "delete_user_confirmation": "Ne sei completamente sicuro? Quest'azione non può essere annullata.", + "delete_user_confirmation": "Ne sei completamente sicuro? Non potrai tornare indietro.", "delete_user": "Elimina utente", "quarantine": "I messaggi non arriveranno alle altre stanze", "disable_any_subscription": "Rendi utente non seguibile", "disable_remote_subscription": "Blocca i tentativi di seguirlo da altre stanze", "sandbox": "Rendi tutti i messaggi solo per seguaci", - "force_unlisted": "Rendi tutti i messaggi invisibili", + "force_unlisted": "Nascondi tutti i messaggi", "strip_media": "Rimuovi ogni allegato ai messaggi", "force_nsfw": "Oscura tutti i messaggi", "delete_account": "Elimina profilo", @@ -469,7 +505,7 @@ }, "show_repeats": "Mostra condivisioni", "hide_repeats": "Nascondi condivisioni", - "mute_progress": "Zittisco…", + "mute_progress": "Silenzio…", "unmute_progress": "Riabilito…", "unmute": "Riabilita", "block_progress": "Blocco…", @@ -478,20 +514,23 @@ "unsubscribe": "Disdici", "subscribe": "Abbònati", "report": "Segnala", - "mention": "Menzioni", + "mention": "Menziona", "media": "Media", "its_you": "Sei tu!", "hidden": "Nascosto", "follow_unfollow": "Disconosci", - "follow_again": "Reinvio richiesta?", "follow_progress": "Richiedo…", "follow_sent": "Richiesta inviata!", "favorites": "Preferiti", "message": "Contatta", - "roles": { - "moderator": "Moderatore", - "admin": "Amministratore" - } + "bot": "Bot", + "highlight": { + "side": "Nastro a lato", + "striped": "A righe", + "solid": "Un colore", + "disabled": "Nessun risalto" + }, + "edit_profile": "Modifica profilo" }, "chat": { "title": "Chat" @@ -547,21 +586,22 @@ "direct": "Diretto - Visibile solo agli utenti menzionati", "private": "Solo per seguaci - Visibile solo dai tuoi seguaci", "public": "Pubblico - Visibile sulla sequenza pubblica", - "unlisted": "Non elencato - Non visibile sulla sequenza pubblica" + "unlisted": "Nascosto - Non visibile sulla sequenza pubblica" }, "scope_notice": { "unlisted": "Questo messaggio non sarà visibile sulla sequenza locale né su quella pubblica", "private": "Questo messaggio sarà visibile solo ai tuoi seguaci", "public": "Questo messaggio sarà visibile a tutti" }, - "direct_warning_to_first_only": "Questo messaggio sarà visibile solo agli utenti menzionati all'inizio.", + "direct_warning_to_first_only": "Questo messaggio sarà visibile solo agli utenti menzionati in testa.", "direct_warning_to_all": "Questo messaggio sarà visibile a tutti i menzionati.", "new_status": "Nuovo messaggio", - "empty_status_error": "Non puoi pubblicare messaggi vuoti senza allegati", + "empty_status_error": "Aggiungi del testo o degli allegati", "preview_empty": "Vuoto", "preview": "Anteprima", "media_description_error": "Allegati non caricati, riprova", - "media_description": "Descrizione allegati" + "media_description": "Descrizione allegati", + "post": "Pubblica" }, "registration": { "bio": "Introduzione", @@ -581,11 +621,14 @@ "bio_placeholder": "es.\nCiao, sono Lupo Lucio.\nSono un lupo fantastico che vive nel Fantabosco. Forse mi hai visto alla Melevisione.", "fullname_placeholder": "es. Lupo Lucio", "username_placeholder": "es. mister_wolf", - "new_captcha": "Clicca l'immagine per avere un altro captcha", - "captcha": "CAPTCHA" + "new_captcha": "Clicca il captcha per averne uno nuovo", + "captcha": "CAPTCHA", + "reason_placeholder": "L'amministratore esamina ciascuna richiesta.\nFornisci il motivo della tua iscrizione.", + "reason": "Motivo dell'iscrizione", + "register": "Registrati" }, "user_profile": { - "timeline_title": "Sequenza dell'Utente", + "timeline_title": "Sequenza dell'utente", "profile_loading_error": "Spiacente, c'è stato un errore nel caricamento del profilo.", "profile_does_not_exist": "Spiacente, questo profilo non esiste." }, @@ -601,7 +644,7 @@ "replace": "Sostituisci", "is_replaced_by": "→", "keyword_policies": "Regole per parole chiave", - "ftl_removal": "Rimozione dalla sequenza globale" + "ftl_removal": "Rimozione dalla sequenza federale" }, "simple": { "reject": "Rifiuta", @@ -611,8 +654,8 @@ "reject_desc": "Questa stanza rifiuterà i messaggi provenienti dalle seguenti:", "quarantine": "Quarantena", "quarantine_desc": "Questa stanza inoltrerà solo messaggi pubblici alle seguenti:", - "ftl_removal": "Rimozione dalla sequenza globale", - "ftl_removal_desc": "Questa stanza rimuove le seguenti dalla sequenza globale:", + "ftl_removal": "Rimozione dalla sequenza federale", + "ftl_removal_desc": "Questa stanza rimuove le seguenti dalla sequenza federale:", "media_removal": "Rimozione multimedia", "media_removal_desc": "Questa istanza rimuove gli allegati dalle seguenti stanze:", "media_nsfw": "Allegati oscurati d'ufficio", @@ -624,8 +667,8 @@ "staff": "Responsabili" }, "domain_mute_card": { - "mute": "Zittisci", - "mute_progress": "Zittisco…", + "mute": "Silenzia", + "mute_progress": "Procedo…", "unmute": "Ascolta", "unmute_progress": "Procedo…" }, @@ -660,11 +703,13 @@ "expiry": "Età ", "expires_in": "Chiude fra {0}", "expired": "Chiuso {0} fa", - "not_enough_options": "Aggiungi altre risposte" + "not_enough_options": "Aggiungi altre risposte", + "votes_count": "{count} voto | {count} voti", + "people_voted_count": "{count} votante | {count} votanti" }, "interactions": { "favs_repeats": "Condivisi e Graditi", - "load_older": "Carica vecchie interazioni", + "load_older": "Carica interazioni precedenti", "moves": "Utenti migrati", "follows": "Nuovi seguìti" }, @@ -699,8 +744,8 @@ "favorites": "Preferiti", "hide_content": "Nascondi contenuti", "show_content": "Mostra contenuti", - "hide_full_subject": "Nascondi intero oggetto", - "show_full_subject": "Mostra intero oggetto", + "hide_full_subject": "Nascondi oggetto intero", + "show_full_subject": "Mostra oggetto intero", "thread_muted_and_words": ", contiene:", "thread_muted": "Discussione silenziata", "copy_link": "Copia collegamento", @@ -708,46 +753,49 @@ "unmute_conversation": "Riabilita conversazione", "mute_conversation": "Silenzia conversazione", "replies_list": "Risposte:", - "reply_to": "Rispondi a", + "reply_to": "In risposta a", "delete_confirm": "Vuoi veramente eliminare questo messaggio?", "unbookmark": "Rimuovi segnalibro", "bookmark": "Aggiungi segnalibro", "status_deleted": "Questo messagio è stato cancellato", - "nsfw": "Pruriginoso", - "external_source": "Vai al sito", - "expand": "Espandi" + "nsfw": "DISDICEVOLE", + "external_source": "Vai all'origine", + "expand": "Espandi", + "mentions": "Menzioni", + "you": "(Tu)", + "plus_more": "+{number} altri" }, "time": { - "years_short": "{0}a", - "year_short": "{0}a", + "years_short": "{0} a", + "year_short": "{0} a", "years": "{0} anni", "year": "{0} anno", - "weeks_short": "{0}set", - "week_short": "{0}set", - "seconds_short": "{0}sec", - "second_short": "{0}sec", + "weeks_short": "{0} stm", + "week_short": "{0} stm", + "seconds_short": "{0} sec", + "second_short": "{0} sec", "weeks": "{0} settimane", "week": "{0} settimana", "seconds": "{0} secondi", "second": "{0} secondo", - "now_short": "ora", + "now_short": "adesso", "now": "adesso", - "months_short": "{0}me", - "month_short": "{0}me", + "months_short": "{0} mes", + "month_short": "{0} mes", "months": "{0} mesi", "month": "{0} mese", - "minutes_short": "{0}min", - "minute_short": "{0}min", + "minutes_short": "{0} min", + "minute_short": "{0} min", "minutes": "{0} minuti", "minute": "{0} minuto", "in_past": "{0} fa", "in_future": "fra {0}", - "hours_short": "{0}h", - "days_short": "{0}g", - "hour_short": "{0}h", + "hours_short": "{0} h", + "days_short": "{0} g", + "hour_short": "{0} h", "hours": "{0} ore", "hour": "{0} ora", - "day_short": "{0}g", + "day_short": "{0} g", "days": "{0} giorni", "day": "{0} giorno" }, @@ -761,7 +809,7 @@ "add_comment_description": "La segnalazione sarà inviata ai moderatori della tua stanza. Puoi motivarla qui sotto:" }, "password_reset": { - "password_reset_required_but_mailer_is_disabled": "Devi reimpostare la tua password, ma non puoi farlo. Contatta il tuo amministratore.", + "password_reset_required_but_mailer_is_disabled": "Devi reimpostare la tua password, ma non puoi farlo. Contatta l'amministratore.", "password_reset_required": "Devi reimpostare la tua password per poter continuare.", "password_reset_disabled": "Non puoi azzerare la tua password. Contatta il tuo amministratore.", "too_many_requests": "Hai raggiunto il numero massimo di tentativi, riprova più tardi.", @@ -802,7 +850,7 @@ "add_reaction": "Reagisci", "favorite": "Gradisci", "reply": "Rispondi", - "repeat": "Ripeti", + "repeat": "Condividi", "media_upload": "Carica allegati" }, "display_date": { diff --git a/src/i18n/ja_easy.json b/src/i18n/ja_easy.json index 991f3762f260d947d8c87e1a9606b3336c3bd996..f64943d9a30bc173e2f63cbcb5f963fb8adabe63 100644 --- a/src/i18n/ja_easy.json +++ b/src/i18n/ja_easy.json @@ -567,7 +567,6 @@ "follow": "フォãƒãƒ¼", "follow_sent": "リクエストをã€ãŠãã‚Šã¾ã—ãŸï¼", "follow_progress": "リクエストã—ã¦ã„ã¾ã™â€¦", - "follow_again": "ãµãŸãŸã³ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’ãŠãã‚Šã¾ã™ã‹ï¼Ÿ", "follow_unfollow": "フォãƒãƒ¼ã‚’ã‚„ã‚ã‚‹", "followees": "フォãƒãƒ¼", "followers": "フォãƒãƒ¯ãƒ¼", diff --git a/src/i18n/ja_pedantic.json b/src/i18n/ja_pedantic.json index e2de10669dcaf17b1cf3724937a25b894ece1325..be3346514adb5ce05d109000218c646858e7b2e2 100644 --- 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,20 @@ "disable": "無効", "enable": "有効", "confirm": "確èª", - "verify": "検査" + "verify": "検査", + "peek": "éš ã™", + "close": "é–‰ã˜ã‚‹", + "dismiss": "無視", + "retry": "ã‚‚ã†ä¸€åº¦ãŠè©¦ã—下ã•ã„", + "error_retry": "ã‚‚ã†ä¸€åº¦ãŠè©¦ã—下ã•ã„", + "loading": "èªã¿è¾¼ã¿ä¸â€¦", + "role": { + "moderator": "モデレーター", + "admin": "管ç†è€…" + }, + "flash_security": "FlashコンテンツãŒä»»æ„ã®å‘½ä»¤ã‚’実行ã•ã›ã‚‹ã“ã¨ã«ã‚ˆã‚Šã€ã‚³ãƒ³ãƒ”ューターãŒå±é™ºã«ã•ã‚‰ã•ã‚Œã‚‹ã“ã¨ãŒã‚ã‚Šã¾ã™ã€‚", + "flash_fail": "Flashコンテンツã®èªã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸã€‚コンソールã§è©³ç´°ã‚’確èªã§ãã¾ã™ã€‚", + "flash_content": "(試験的機能)クリックã—ã¦Flashコンテンツをå†ç”Ÿã—ã¾ã™ã€‚" }, "image_cropper": { "crop_picture": "ç”»åƒã‚’切り抜ã", @@ -57,9 +72,9 @@ "enter_recovery_code": "リカãƒãƒªãƒ¼ã‚³ãƒ¼ãƒ‰ã‚’入力ã—ã¦ãã ã•ã„", "enter_two_factor_code": "2段階èªè¨¼ã‚³ãƒ¼ãƒ‰ã‚’入力ã—ã¦ãã ã•ã„", "recovery_code": "リカãƒãƒªãƒ¼ã‚³ãƒ¼ãƒ‰", - "heading" : { - "totp" : "2段階èªè¨¼", - "recovery" : "2段階リカãƒãƒªãƒ¼" + "heading": { + "totp": "2段階èªè¨¼", + "recovery": "2段階リカãƒãƒªãƒ¼" } }, "media_modal": { @@ -74,23 +89,32 @@ "mentions": "通知", "interactions": "インタラクション", "dms": "ダイレクトメッセージ", - "public_tl": "パブリックタイムライン", + "public_tl": "公開タイムライン", "timeline": "タイムライン", - "twkn": "接続ã—ã¦ã„ã‚‹ã™ã¹ã¦ã®ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯", + "twkn": "ã™ã¹ã¦ã®ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯", "user_search": "ユーザーを探ã™", "search": "検索", "who_to_follow": "ãŠã™ã™ã‚ユーザー", - "preferences": "è¨å®š" + "preferences": "è¨å®š", + "administration": "管ç†", + "bookmarks": "ブックマーク", + "timelines": "タイムライン", + "chats": "ãƒãƒ£ãƒƒãƒˆ", + "home_timeline": "ホームタイムライン" }, "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 +128,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 +139,9 @@ "search_emoji": "絵文å—を検索", "add_emoji": "絵文å—を挿入", "custom": "カスタム絵文å—", - "unicode": "Unicode絵文å—" + "unicode": "Unicode絵文å—", + "load_all": "å…¨ {emojiAmount} 絵文å—ã‚’èªã¿è¾¼ã‚€", + "load_all_hint": "最åˆã® {saneAmount} 絵文å—ã‚’èªã¿è¾¼ã¿ã¾ã—ãŸã€å…¨ã¦èªã¿è¾¼ã‚€ã¨é‡ããªã‚‹å¯èƒ½æ€§ãŒã‚ã‚Šã¾ã™ã€‚" }, "stickers": { "add_sticker": "ã‚¹ãƒ†ãƒƒã‚«ãƒ¼ã‚’è¿½åŠ " @@ -121,7 +149,8 @@ "interactions": { "favs_repeats": "リピートã¨ãŠæ°—ã«å…¥ã‚Š", "follows": "æ–°ã—ã„フォãƒãƒ¯ãƒ¼", - "load_older": "å¤ã„インタラクションを見る" + "load_older": "å¤ã„インタラクションを見る", + "moves": "ユーザーã®å¼•ã£è¶Šã—" }, "post_status": { "new_status": "投稿ã™ã‚‹", @@ -142,15 +171,21 @@ "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": "メディアã®èª¬æ˜Ž", + "post": "投稿" }, "registration": { "bio": "プãƒãƒ•ã‚£ãƒ¼ãƒ«", @@ -171,7 +206,10 @@ "password_required": "å¿…é ˆ", "password_confirmation_required": "å¿…é ˆ", "password_confirmation_match": "パスワードãŒé•ã„ã¾ã™" - } + }, + "reason_placeholder": "ã“ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã¯ã€æ–°è¦ç™»éŒ²ã‚’手動ã§å—ã‘付ã‘ã¦ã„ã¾ã™ã€‚\n登録ã—ãŸã„ç†ç”±ã‚’ã€ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã®ç®¡ç†è€…ã«æ•™ãˆã¦ãã ã•ã„。", + "reason": "登録ã™ã‚‹ãŸã‚ã®ç›®çš„", + "register": "登録" }, "selectable_list": { "select_all": "ã™ã¹ã¦é¸æŠž" @@ -181,17 +219,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 +269,7 @@ "data_import_export_tab": "インãƒãƒ¼ãƒˆã¨ã‚¨ã‚¯ã‚¹ãƒãƒ¼ãƒˆ", "default_vis": "デフォルトã®å…¬é–‹ç¯„囲", "delete_account": "アカウントを消ã™", - "delete_account_description": "ã‚ãªãŸã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã¨ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ãŒã€æ¶ˆãˆã¾ã™ã€‚", + "delete_account_description": "ã‚ãªãŸã®ãƒ‡ãƒ¼ã‚¿ãŒæ¶ˆãˆã¦ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆãŒä½¿ãˆãªããªã‚Šã¾ã™ã€‚", "delete_account_error": "アカウントを消ã™ã“ã¨ãŒã€ã§ããªã‹ã£ãŸã‹ã‚‚ã—ã‚Œã¾ã›ã‚“。インスタンスã®ç®¡ç†è€…ã«ã€é€£çµ¡ã—ã¦ãã ã•ã„。", "delete_account_instructions": "本当ã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’消ã—ã¦ã‚‚ã„ã„ãªã‚‰ã€ãƒ‘スワードを入力ã—ã¦ãã ã•ã„。", "discoverable": "検索ãªã©ã®ã‚µãƒ¼ãƒ“スã§ã“ã®ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚’見ã¤ã‘ã‚‹ã“ã¨ã‚’許å¯ã™ã‚‹", @@ -239,12 +277,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": "全般", @@ -291,8 +329,8 @@ "hide_followers_description": "フォãƒãƒ¯ãƒ¼ã‚’見ã›ãªã„", "hide_follows_count_description": "フォãƒãƒ¼ã—ã¦ã„る人ã®æ•°ã‚’見ã›ãªã„", "hide_followers_count_description": "フォãƒãƒ¯ãƒ¼ã®æ•°ã‚’見ã›ãªã„", - "show_admin_badge": "管ç†è€…ã®ãƒãƒƒã‚¸ã‚’見ã›ã‚‹", - "show_moderator_badge": "モデレーターã®ãƒãƒƒã‚¸ã‚’見ã›ã‚‹", + "show_admin_badge": "\"管ç†è€…\"ã®ãƒãƒƒã‚¸ã‚’見ã›ã‚‹", + "show_moderator_badge": "\"モデレーター\"ã®ãƒãƒƒã‚¸ã‚’見ã›ã‚‹", "nsfw_clickthrough": "NSFWãªãƒ•ã‚¡ã‚¤ãƒ«ã‚’éš ã™", "oauth_tokens": "OAuthトークン", "token": "トークン", @@ -302,10 +340,10 @@ "panelRadius": "パãƒãƒ«", "pause_on_unfocused": "タブã«ãƒ•ã‚©ãƒ¼ã‚«ã‚¹ãŒãªã„ã¨ãストリーミングをæ¢ã‚ã‚‹", "presets": "プリセット", - "profile_background": "プãƒãƒ•ã‚£ãƒ¼ãƒ«ã®ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰", - "profile_banner": "プãƒãƒ•ã‚£ãƒ¼ãƒ«ãƒãƒŠãƒ¼", + "profile_background": "プãƒãƒ•ã‚£ãƒ¼ãƒ«ã®èƒŒæ™¯", + "profile_banner": "プãƒãƒ•ã‚£ãƒ¼ãƒ«ã®ãƒãƒŠãƒ¼", "profile_tab": "プãƒãƒ•ã‚£ãƒ¼ãƒ«", - "radii_help": "インターフェースã®ä¸¸ã•ã‚’è¨å®šã™ã‚‹ã€‚", + "radii_help": "インターフェースã®ä¸¸ã•ã‚’è¨å®šã™ã‚‹", "replies_in_timeline": "タイムラインã®ãƒªãƒ—ライ", "reply_visibility_all": "ã™ã¹ã¦ã®ãƒªãƒ—ライを見る", "reply_visibility_following": "ç§ã«å®›ã¦ã‚‰ã‚ŒãŸãƒªãƒ—ライã¨ã€ãƒ•ã‚©ãƒãƒ¼ã—ã¦ã„る人ã‹ã‚‰ã®ãƒªãƒ—ライを見る", @@ -332,7 +370,7 @@ "streaming": "上ã¾ã§ã‚¹ã‚¯ãƒãƒ¼ãƒ«ã—ãŸã¨ãã€è‡ªå‹•çš„ã«ã‚¹ãƒˆãƒªãƒ¼ãƒŸãƒ³ã‚°ã™ã‚‹", "text": "æ–‡å—", "theme": "テーマ", - "theme_help": "カラーテーマをカスタマイズã§ãã¾ã™", + "theme_help": "カラーテーマをカスタマイズã§ãã¾ã™ã€‚", "theme_help_v2_1": "ãƒã‚§ãƒƒã‚¯ãƒœãƒƒã‚¯ã‚¹ã‚’ONã«ã™ã‚‹ã¨ã€ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆã”ã¨ã«ã€è‰²ã¨é€æ˜Žåº¦ã‚’オーãƒãƒ¼ãƒ©ã‚¤ãƒ‰ã§ãã¾ã™ã€‚「ã™ã¹ã¦ã‚¯ãƒªã‚¢ã€ãƒœã‚¿ãƒ³ã‚’押ã™ã¨ã€ã™ã¹ã¦ã®ã‚ªãƒ¼ãƒãƒ¼ãƒ©ã‚¤ãƒ‰ã‚’ã‚„ã‚ã¾ã™ã€‚", "theme_help_v2_2": "ãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã¨ãƒ†ã‚ストã®ã‚³ãƒ³ãƒˆãƒ©ã‚¹ãƒˆã‚’表ã™ã‚¢ã‚¤ã‚³ãƒ³ãŒã‚ã‚Šã¾ã™ã€‚マウスをホãƒãƒ¼ã™ã‚‹ã¨ã€è©³ã—ã„説明ãŒå‡ºã¾ã™ã€‚é€æ˜Žãªè‰²ã‚’使ã£ã¦ã„ã‚‹ã¨ãã¯ã€æœ€æ‚ªã®å ´åˆã®ã‚³ãƒ³ãƒˆãƒ©ã‚¹ãƒˆãŒç¤ºã•ã‚Œã¾ã™ã€‚", "tooltipRadius": "ツールãƒãƒƒãƒ—ã¨ã‚¢ãƒ©ãƒ¼ãƒˆ", @@ -356,7 +394,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 +419,9 @@ "contrast": { "hint": "コントラスト㯠{ratio} ã§ã™ã€‚{level}。({context})", "level": { - "aa": "AAレベルガイドライン (ミニマル) を満ãŸã—ã¾ã™", - "aaa": "AAAレベルガイドライン (レコメンデッド) を満ãŸã—ã¾ã™ã€‚", - "bad": "ガイドラインを満ãŸã—ã¾ã›ã‚“。" + "aa": "AAレベルガイドライン (最低é™) を満ãŸã—ã¾ã™", + "aaa": "AAAレベルガイドライン (推奨) を満ãŸã—ã¾ã™", + "bad": "ガイドラインを満ãŸã—ã¾ã›ã‚“" }, "context": { "18pt": "大ãã„ (18ãƒã‚¤ãƒ³ãƒˆä»¥ä¸Š) テã‚スト", @@ -391,7 +446,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 +484,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 +499,8 @@ "buttonPressed": "ボタン (押ã•ã‚Œã¦ã„ã‚‹ã¨ã)", "buttonPressedHover": "ボタン (ホãƒãƒ¼ã€ã‹ã¤ã€æŠ¼ã•ã‚Œã¦ã„ã‚‹ã¨ã)", "input": "インプットフィールド" - } + }, + "hintV3": "å½±ã®å ´åˆã¯ã€ {0} 表記を使ã£ã¦ä»–ã®è‰²ã‚¹ãƒãƒƒãƒˆã‚’使ã†ã“ã¨ã‚‚ã§ãã¾ã™ã€‚" }, "fonts": { "_tab_label": "フォント", @@ -445,7 +521,7 @@ "content": "本文", "error": "エラーã®ä¾‹", "button": "ボタン", - "text": "ã“ã‚Œã¯{0}ã¨{1}ã®ä¾‹ã§ã™ã€‚", + "text": "ã“ã‚Œã¯{0}ã¨{1}ã®ä¾‹ã§ã™", "mono": "monospace", "input": "羽田空港ã«ç€ãã¾ã—ãŸã€‚", "faint_link": "ã¨ã¦ã‚‚助ã‘ã«ãªã‚‹ãƒžãƒ‹ãƒ¥ã‚¢ãƒ«", @@ -459,7 +535,72 @@ "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": "ã¯ã˜ã‚ã‹ã‚‰æŠ•ç¨¿ã‚’センシティブã¨ã—ã¦è¨å®š", + "more_settings": "ãã®ä»–ã®è¨å®š", + "reply_visibility_self_short": "自分宛ã®ãƒªãƒ—ライを見る", + "reply_visibility_following_short": "フォãƒãƒ¼ã—ã¦ã„る人ã«å®›ã¦ã‚‰ã‚ŒãŸãƒªãƒ—ライを見る", + "hide_all_muted_posts": "ミュートã—ãŸæŠ•ç¨¿ã‚’éš ã™", + "hide_media_previews": "メディアã®ãƒ—ãƒ¬ãƒ“ãƒ¥ãƒ¼ã‚’éš ã™", + "word_filter": "å˜èªžãƒ•ã‚£ãƒ«ã‚¿", + "file_export_import": { + "errors": { + "invalid_file": "ã“ã‚Œã¯Pleromaã®è¨å®šã‚’ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—ã—ãŸãƒ•ã‚¡ã‚¤ãƒ«ã§ã¯ã‚ã‚Šã¾ã›ã‚“。", + "file_slightly_new": "ファイルã®ãƒžã‚¤ãƒŠãƒ¼ãƒãƒ¼ã‚¸ãƒ§ãƒ³ãŒç•°ãªã‚Šã€ä¸€éƒ¨ã®è¨å®šãŒèªã¿è¾¼ã¾ã‚Œãªã„ã“ã¨ãŒã‚ã‚Šã¾ã™" + }, + "restore_settings": "è¨å®šã‚’ファイルã‹ã‚‰å¾©å…ƒã™ã‚‹", + "backup_settings_theme": "テーマをå«ã‚€è¨å®šã‚’ファイルã«ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—ã™ã‚‹", + "backup_settings": "è¨å®šã‚’ファイルã«ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—ã™ã‚‹", + "backup_restore": "è¨å®šã‚’ãƒãƒƒã‚¯ã‚¢ãƒƒãƒ—" + }, + "save": "変更をä¿å˜", + "hide_shoutbox": "Shoutboxを表示ã—ãªã„", + "always_show_post_button": "投稿ボタンを常ã«è¡¨ç¤º", + "right_sidebar": "サイドãƒãƒ¼ã‚’å³ã«è¡¨ç¤º" }, "time": { "day": "{0}æ—¥", @@ -505,7 +646,11 @@ "show_new": "èªã¿è¾¼ã¿", "up_to_date": "最新", "no_more_statuses": "ã“ã‚Œã§çµ‚ã‚ã‚Šã§ã™", - "no_statuses": "ステータスã¯ã‚ã‚Šã¾ã›ã‚“" + "no_statuses": "ステータスã¯ã‚ã‚Šã¾ã›ã‚“", + "reload": "å†èªã¿è¾¼ã¿", + "error": "タイムラインã®èªã¿è¾¼ã¿ã«å¤±æ•—ã—ã¾ã—ãŸ: {0}", + "socket_reconnected": "リアルタイム接続ãŒç¢ºç«‹ã•ã‚Œã¾ã—ãŸ", + "socket_broke": "コード{0}ã«ã‚ˆã‚Šãƒªã‚¢ãƒ«ã‚¿ã‚¤ãƒ 接続ãŒåˆ‡æ–ã•ã‚Œã¾ã—ãŸ" }, "status": { "favorites": "ãŠæ°—ã«å…¥ã‚Š", @@ -518,7 +663,24 @@ "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": "ブックマーク", + "mentions": "メンション", + "you": "(ã‚ãªãŸï¼‰", + "plus_more": "ã»ã‹{number}件" }, "user_card": { "approve": "å—ã‘入れ", @@ -529,7 +691,6 @@ "follow": "フォãƒãƒ¼", "follow_sent": "リクエストをé€ã‚Šã¾ã—ãŸï¼", "follow_progress": "リクエストã—ã¦ã„ã¾ã™â€¦", - "follow_again": "å†ã³ãƒªã‚¯ã‚¨ã‚¹ãƒˆã‚’é€ã‚Šã¾ã™ã‹ï¼Ÿ", "follow_unfollow": "フォãƒãƒ¼ã‚’ã‚„ã‚ã‚‹", "followees": "フォãƒãƒ¼", "followers": "フォãƒãƒ¯ãƒ¼", @@ -539,7 +700,7 @@ "media": "メディア", "mention": "メンション", "mute": "ミュート", - "muted": "ミュートã—ã¦ã„ã¾ã™ï¼", + "muted": "ミュートã—ã¦ã„ã¾ã™", "per_day": "/æ—¥", "remote_follow": "リモートフォãƒãƒ¼", "report": "é€šå ±", @@ -547,11 +708,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 +731,23 @@ "quarantine": "ä»–ã®ã‚¤ãƒ³ã‚¹ã‚¿ãƒ³ã‚¹ã‹ã‚‰ã®æŠ•ç¨¿ã‚’æ¢ã‚ã‚‹", "delete_user": "ユーザーを削除", "delete_user_confirmation": "ã‚ãªãŸã®ç²¾ç¥žçŠ¶æ…‹ã«ä½•ã‹å•é¡Œã¯ã”ã–ã„ã¾ã›ã‚“ã‹ï¼Ÿ ã“ã®æ“作をå–り消ã™ã“ã¨ã¯ã§ãã¾ã›ã‚“。" - } + }, + "roles": { + "moderator": "モデレーター", + "admin": "管ç†è€…" + }, + "show_repeats": "リピートを見る", + "hide_repeats": "ãƒªãƒ”ãƒ¼ãƒˆã‚’éš ã™", + "message": "メッセージ", + "hidden": "éš ã™", + "bot": "bot", + "highlight": { + "solid": "背景をå˜è‰²ã«ã™ã‚‹", + "striped": "背景を縞模様ã«ã™ã‚‹", + "side": "端ã«ç·šã‚’付ã‘ã‚‹", + "disabled": "強調ã—ãªã„" + }, + "edit_profile": "プãƒãƒ•ã‚£ãƒ¼ãƒ«ã‚’編集" }, "user_profile": { "timeline_title": "ユーザータイムライン", @@ -595,13 +772,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 +808,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 index 0968949b390946a1e52fa5799ae81fb8a7dad954..cd0cb9925704d1397dc29f1ae7348dfbc6cc15e4 100644 --- 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,31 @@ "dms": "다ì´ë ‰íŠ¸ 메시지", "public_tl": "공개 타임ë¼ì¸", "timeline": "타임ë¼ì¸", - "twkn": "ëª¨ë“ ì•Œë ¤ì§„ 네트워í¬", + "twkn": "ì•Œë ¤ì§„ 네트워í¬", "user_search": "ì‚¬ìš©ìž ê²€ìƒ‰", - "preferences": "í™˜ê²½ì„¤ì •" + "preferences": "í™˜ê²½ì„¤ì •", + "chats": "채트", + "timelines": "타임ë¼ì¸", + "who_to_follow": "ì¶”ì²œëœ ì‚¬ìš©ìž", + "search": "검색", + "bookmarks": "ë¶ë§ˆí¬", + "interactions": "대화", + "administration": "관리", + "home_timeline": "홈 타임ë¼ì¸" }, "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}", + "follow_request": "ë‹¹ì‹ ì—게 팔로우 ì‹ ì²" }, "post_status": { "new_status": "새 게시물 게시", @@ -56,10 +100,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 +114,15 @@ "private": "팔로워 ì „ìš© - 팔로워들ì—게만", "public": "공개 - 공개 타임ë¼ì¸ìœ¼ë¡œ", "unlisted": "비공개 - 공개 타임ë¼ì¸ì— 게시 안 함" - } + }, + "preview_empty": "ì•„ë¬´ê²ƒë„ ì—†ìŠµë‹ˆë‹¤", + "preview": "미리보기", + "scope_notice": { + "public": "ì´ ê¸€ì€ ëˆ„êµ¬ë‚˜ ë³¼ 수 있습니다" + }, + "media_description_error": "파ì¼ì„ 올리지 못하였습니다. 다시한번 ì‹œë„하여 주ì‹ì‹œì˜¤", + "empty_status_error": "ê¸€ì„ ìž…ë ¥í•˜ì‹ì‹œì˜¤", + "media_description": "ì²¨ë¶€íŒŒì¼ ì„¤ëª…" }, "registration": { "bio": "소개", @@ -85,7 +140,9 @@ "password_required": "공백으로 둘 수 없습니다", "password_confirmation_required": "공백으로 둘 수 없습니다", "password_confirmation_match": "패스워드와 ì¼ì¹˜í•´ì•¼ 합니다" - } + }, + "fullname_placeholder": "예: 김례ì¸", + "username_placeholder": "예: lain" }, "settings": { "attachmentRadius": "첨부물", @@ -112,7 +169,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 +213,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 +304,16 @@ "borders": "í…Œë‘리", "buttons": "버튼", "inputs": "ìž…ë ¥ì¹¸", - "faint_text": "íë ¤ì§„ í…스트" + "faint_text": "íë ¤ì§„ í…스트", + "chat": { + "border": "ê²½ê³„ì„ ", + "outgoing": "ì†¡ì‹ ", + "incoming": "ìˆ˜ì‹ " + }, + "selectedMenu": "ì„ íƒëœ 메뉴 요소", + "selectedPost": "ì„ íƒëœ 글", + "icons": "ì•„ì´ì½˜", + "alert_warning": "ê²½ê³ " }, "radii": { "_tab_label": "둥글기" @@ -303,14 +369,46 @@ "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": "침묵", + "app_name": "앱 ì´ë¦„" }, "timeline": { "collapse": "ì ‘ê¸°", @@ -330,7 +428,6 @@ "follow": "팔로우", "follow_sent": "ìš”ì² ë³´ë‚´ì§!", "follow_progress": "ìš”ì² ì¤‘â€¦", - "follow_again": "ìš”ì²ì„ 다시 보낼까요?", "follow_unfollow": "팔로우 중지", "followees": "팔로우 중", "followers": "팔로워", @@ -339,7 +436,7 @@ "its_you": "ë‹¹ì‹ ìž…ë‹ˆë‹¤!", "mute": "침묵", "muted": "침묵 ë¨", - "per_day": " / 하루", + "per_day": "/ 하루", "remote_follow": "ì›ê²© 팔로우", "statuses": "게시물" }, @@ -357,11 +454,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 +467,129 @@ "GiB": "기비바ì´íŠ¸", "TiB": "테비바ì´íŠ¸" } + }, + "interactions": { + "follows": "새 팔로워", + "favs_repeats": "반복과 ì¦ê²¨ì°¾ê¸°", + "moves": "ê³„ì • 통합" + }, + "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": "ì„ íƒì§€ 추가", + "expired": "투표는 {0} ì „ì— ë§ˆê°ë˜ì—ˆìŠµë‹ˆë‹¤", + "expires_in": "투표는 {0}ì— ë§ˆê°ë©ë‹ˆë‹¤" + }, + "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": "ì°¾ì„ ìˆ˜ 없습니다.", + "searching_for": "검색중" + }, + "selectable_list": { + "select_all": "ëª¨ë‘ ì„ íƒ" } } diff --git a/src/i18n/nb.json b/src/i18n/nb.json index b9669a351f222881f283b93ede7502735bb6607f..5e3e8ef3da7a917b672ff681f3f6c49734d5109b 100644 --- a/src/i18n/nb.json +++ b/src/i18n/nb.json @@ -41,8 +41,8 @@ }, "importer": { "submit": "Send", - "success": "Importering fullført", - "error": "Det oppsto en feil under importering av denne filen" + "success": "Importering fullført.", + "error": "Det oppsto en feil under importering av denne filen." }, "login": { "login": "Logg inn", @@ -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,17 +80,20 @@ "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...", + "broken_favorite": "Ukjent status, leter etter den…", "favorited_you": "likte din status", "followed_you": "fulgte deg", "load_older": "Last eldre varsler", "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", @@ -444,7 +447,8 @@ "title": "Versjon", "backend_version": "Backend Versjon", "frontend_version": "Frontend Versjon" - } + }, + "hide_wallpaper": "Skjul instansens bakgrunnsbilde" }, "time": { "day": "{0} dag", @@ -512,7 +516,6 @@ "follow": "Følg", "follow_sent": "Forespørsel sendt!", "follow_progress": "Forespør…", - "follow_again": "Gjenta forespørsel?", "follow_unfollow": "Avfølg", "followees": "Følger", "followers": "Følgere", @@ -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}]", @@ -599,5 +602,22 @@ "person_talking": "{count} person snakker om dette", "people_talking": "{count} personer snakker om dette", "no_results": "Ingen resultater" + }, + "about": { + "mrf": { + "simple": { + "quarantine": "Karantene", + "reject_desc": "Denne instansen vil ikke godta meldinger fra følgende instanser:", + "reject": "Avvis", + "accept_desc": "Denne instansen godtar kun meldinger fra følgende instanser:", + "accept": "Aksepter" + }, + "keyword": { + "is_replaced_by": "→", + "replace": "Erstatt", + "reject": "Avvis", + "ftl_removal": "Fjerning fra \"Det hele kjente nettverket\" Tidslinjen" + } + } } } diff --git a/src/i18n/nl.json b/src/i18n/nl.json index 8da07ac7c9559ae06a0597bd3602f1843143be0c..fd61572c2b20cea48b382fc4f51063edcdc4e09b 100644 --- a/src/i18n/nl.json +++ b/src/i18n/nl.json @@ -5,11 +5,13 @@ "features_panel": { "chat": "Chat", "gopher": "Gopher", - "media_proxy": "Media proxy", + "media_proxy": "Mediaproxy", "scope_options": "Zichtbaarheidsopties", - "text_limit": "Tekst limiet", + "text_limit": "Tekstlimiet", "title": "Kenmerken", - "who_to_follow": "Wie te volgen" + "who_to_follow": "Wie te volgen", + "upload_limit": "Upload limiet", + "pleroma_chat_messages": "Pleroma Chat" }, "finder": { "error_fetching_user": "Fout tijdens ophalen gebruiker", @@ -17,11 +19,11 @@ }, "general": { "apply": "Toepassen", - "submit": "Verzend", + "submit": "Verzenden", "more": "Meer", "optional": "optioneel", - "show_more": "Bekijk meer", - "show_less": "Bekijk minder", + "show_more": "Meer tonen", + "show_less": "Minder tonen", "dismiss": "Opheffen", "cancel": "Annuleren", "disable": "Uitschakelen", @@ -29,28 +31,32 @@ "confirm": "Bevestigen", "verify": "Verifiëren", "generic_error": "Er is een fout opgetreden", - "peek": "Spiek", + "peek": "Spieken", "close": "Sluiten", "retry": "Opnieuw proberen", "error_retry": "Probeer het opnieuw", - "loading": "Laden…" + "loading": "Laden…", + "role": { + "moderator": "Moderator", + "admin": "Beheerder" + } }, "login": { - "login": "Log in", - "description": "Log in met OAuth", + "login": "Inloggen", + "description": "Inloggen met OAuth", "logout": "Uitloggen", "password": "Wachtwoord", - "placeholder": "bijv. lain", + "placeholder": "bijv. barbapapa", "register": "Registreren", "username": "Gebruikersnaam", "hint": "Log in om deel te nemen aan de discussie", - "authentication_code": "Authenticatie code", + "authentication_code": "Authenticatiecode", "enter_recovery_code": "Voer een herstelcode in", - "enter_two_factor_code": "Voer een twee-factor code in", + "enter_two_factor_code": "Voer een twee-factorcode in", "recovery_code": "Herstelcode", "heading": { - "totp": "Twee-factor authenticatie", - "recovery": "Twee-factor herstelling" + "totp": "Twee-factorauthenticatie", + "recovery": "Twee-factorherstelling" } }, "nav": { @@ -59,35 +65,40 @@ "chat": "Lokale Chat", "friend_requests": "Volgverzoeken", "mentions": "Vermeldingen", - "dms": "Directe Berichten", - "public_tl": "Publieke Tijdlijn", + "dms": "Privéberichten", + "public_tl": "Openbare tijdlijn", "timeline": "Tijdlijn", - "twkn": "Het Geheel Bekende Netwerk", + "twkn": "Bekende Netwerk", "user_search": "Gebruiker Zoeken", "who_to_follow": "Wie te volgen", "preferences": "Voorkeuren", - "administration": "Administratie", + "administration": "Beheer", "search": "Zoeken", - "interactions": "Interacties" + "interactions": "Interacties", + "chats": "Chats", + "home_timeline": "Thuis tijdlijn", + "timelines": "Tijdlijnen", + "bookmarks": "Bladwijzers" }, "notifications": { "broken_favorite": "Onbekende status, aan het zoeken…", "favorited_you": "vond je status leuk", "followed_you": "volgt jou", - "load_older": "Laad oudere meldingen", + "load_older": "Oudere meldingen laden", "notifications": "Meldingen", "read": "Gelezen!", - "repeated_you": "Herhaalde je status", + "repeated_you": "herhaalde je status", "no_more_notifications": "Geen meldingen meer", "migrated_to": "is gemigreerd naar", "follow_request": "wil je volgen", - "reacted_with": "reageerde met {0}" + "reacted_with": "reageerde met {0}", + "error": "Fout bij ophalen van meldingen: {0}" }, "post_status": { "new_status": "Nieuwe status plaatsen", - "account_not_locked_warning": "Je account is niet {0}. Iedereen kan je volgen om je alleen-volgers berichten te lezen.", + "account_not_locked_warning": "Je account is niet {0}. Iedereen kan je volgen om je alleen-volgers-berichten te lezen.", "account_not_locked_warning_link": "gesloten", - "attachments_sensitive": "Markeer bijlagen als gevoelig", + "attachments_sensitive": "Bijlagen als gevoelig markeren", "content_type": { "text/plain": "Platte tekst", "text/html": "HTML", @@ -99,26 +110,32 @@ "direct_warning": "Deze post zal enkel zichtbaar zijn voor de personen die genoemd zijn.", "posting": "Plaatsen", "scope": { - "direct": "Direct - Post enkel naar vermelde gebruikers", - "private": "Enkel volgers - Post enkel naar volgers", - "public": "Publiek - Post op publieke tijdlijnen", - "unlisted": "Niet Vermelden - Niet tonen op publieke tijdlijnen" + "direct": "Privé - bericht enkel naar vermelde gebruikers sturen", + "private": "Enkel volgers - bericht enkel naar volgers sturen", + "public": "Openbaar - bericht op openbare tijdlijnen plaatsen", + "unlisted": "Niet vermelden - niet tonen op openbare tijdlijnen" }, "direct_warning_to_all": "Dit bericht zal zichtbaar zijn voor alle vermelde gebruikers.", "direct_warning_to_first_only": "Dit bericht zal alleen zichtbaar zijn voor de vermelde gebruikers aan het begin van het bericht.", "scope_notice": { "public": "Dit bericht zal voor iedereen zichtbaar zijn", - "unlisted": "Dit bericht zal niet zichtbaar zijn in de Publieke Tijdlijn en Het Geheel Bekende Netwerk", + "unlisted": "Dit bericht zal niet zichtbaar zijn in de Openbare Tijdlijn en Het Geheel Bekende Netwerk", "private": "Dit bericht zal voor alleen je volgers zichtbaar zijn" - } + }, + "post": "Bericht", + "empty_status_error": "Kan geen lege status zonder bijlagen plaatsen", + "preview_empty": "Leeg", + "preview": "Voorbeeld", + "media_description": "Mediaomschrijving", + "media_description_error": "Kon media niet ophalen, probeer het opnieuw" }, "registration": { "bio": "Bio", - "email": "Email", - "fullname": "Weergave naam", + "email": "E-mail", + "fullname": "Weergavenaam", "password_confirm": "Wachtwoord bevestiging", "registration": "Registratie", - "token": "Uitnodigings-token", + "token": "Uitnodigingstoken", "captcha": "CAPTCHA", "new_captcha": "Klik op de afbeelding voor een nieuwe captcha", "validations": { @@ -131,13 +148,16 @@ }, "username_placeholder": "bijv. lain", "fullname_placeholder": "bijv. Lain Iwakura", - "bio_placeholder": "bijv.\nHallo, ik ben Lain.\nIk ben een anime meisje woonachtig in een buitenwijk in Japan. Je kent me misschien van the Wired." + "bio_placeholder": "bijv.\nHallo, ik ben Lain.\nIk ben een animemeisje woonachtig in een buitenwijk in Japan. Je kent me misschien van the Wired.", + "reason_placeholder": "Deze instantie keurt registraties handmatig goed.\nLaat de beheerder weten waarom je wilt registreren.", + "reason": "Reden voor registratie", + "register": "Registreren" }, "settings": { "attachmentRadius": "Bijlages", "attachments": "Bijlages", "avatar": "Avatar", - "avatarAltRadius": "Avatars (Meldingen)", + "avatarAltRadius": "Avatars (meldingen)", "avatarRadius": "Avatars", "background": "Achtergrond", "bio": "Bio", @@ -146,7 +166,7 @@ "cGreen": "Groen (Herhalen)", "cOrange": "Oranje (Favoriet)", "cRed": "Rood (Annuleren)", - "change_password": "Wachtwoord Wijzigen", + "change_password": "Wachtwoord wijzigen", "change_password_error": "Er is een fout opgetreden bij het wijzigen van je wachtwoord.", "changed_password": "Wachtwoord succesvol gewijzigd!", "collapse_subject": "Klap berichten met een onderwerp in", @@ -155,30 +175,30 @@ "current_avatar": "Je huidige avatar", "current_password": "Huidig wachtwoord", "current_profile_banner": "Je huidige profiel banner", - "data_import_export_tab": "Data Import / Export", + "data_import_export_tab": "Data-import / export", "default_vis": "Standaard zichtbaarheidsbereik", - "delete_account": "Account Verwijderen", + "delete_account": "Account verwijderen", "delete_account_description": "Permanent je gegevens verwijderen en account deactiveren.", "delete_account_error": "Er is een fout opgetreden bij het verwijderen van je account. Indien dit probleem zich voor blijft doen, neem dan contact op met de beheerder van deze instantie.", "delete_account_instructions": "Voer je wachtwoord in het onderstaande invoerveld in om het verwijderen van je account te bevestigen.", - "export_theme": "Preset opslaan", + "export_theme": "Voorinstelling opslaan", "filtering": "Filtering", - "filtering_explanation": "Alle statussen die deze woorden bevatten worden genegeerd, één filter per lijn", + "filtering_explanation": "Alle statussen die deze woorden bevatten worden genegeerd, één filter per regel", "follow_export": "Volgers exporteren", - "follow_export_button": "Exporteer je volgers naar een csv bestand", + "follow_export_button": "Exporteer je volgers naar een csv-bestand", "follow_export_processing": "Aan het verwerken, binnen enkele ogenblikken wordt je gevraagd je bestand te downloaden", "follow_import": "Volgers importeren", "follow_import_error": "Fout bij importeren volgers", "follows_imported": "Volgers geïmporteerd! Het kan even duren voordat deze verwerkt zijn.", "foreground": "Voorgrond", "general": "Algemeen", - "hide_attachments_in_convo": "Verberg bijlages in conversaties", - "hide_attachments_in_tl": "Verberg bijlages in de tijdlijn", - "hide_isp": "Verberg instantie-specifiek paneel", + "hide_attachments_in_convo": "Bijlagen in conversaties verbergen", + "hide_attachments_in_tl": "Bijlagen in tijdlijn verbergen", + "hide_isp": "Instantie-specifiek paneel verbergen", "preload_images": "Afbeeldingen vooraf laden", - "hide_post_stats": "Verberg bericht statistieken (bijv. het aantal favorieten)", - "hide_user_stats": "Verberg bericht statistieken (bijv. het aantal volgers)", - "import_followers_from_a_csv_file": "Importeer volgers uit een csv bestand", + "hide_post_stats": "Bericht statistieken verbergen (bijv. het aantal favorieten)", + "hide_user_stats": "Gebruikers-statistieken verbergen (bijv. het aantal volgers)", + "import_followers_from_a_csv_file": "Gevolgden uit een csv bestand importeren", "import_theme": "Preset laden", "inputRadius": "Invoervelden", "checkboxRadius": "Checkboxen", @@ -186,35 +206,35 @@ "instance_default_simple": "(standaard)", "interface": "Interface", "interfaceLanguage": "Interface taal", - "invalid_theme_imported": "Het geselecteerde bestand is geen door Pleroma ondersteund thema. Er zijn geen aanpassingen gedaan.", + "invalid_theme_imported": "Het geselecteerde bestand is niet een door Pleroma ondersteund thema. Er zijn geen aanpassingen gedaan.", "limited_availability": "Niet beschikbaar in je browser", "links": "Links", - "lock_account_description": "Laat volgers enkel toe na expliciete toestemming", - "loop_video": "Herhaal video's", - "loop_video_silent_only": "Herhaal enkel video's zonder geluid (bijv. Mastodon's \"gifs\")", + "lock_account_description": "Volgers enkel na expliciete toestemming toelaten", + "loop_video": "Video's herhalen", + "loop_video_silent_only": "Enkel video's zonder geluid herhalen (bijv. Mastodon's \"gifs\")", "name": "Naam", - "name_bio": "Naam & Bio", + "name_bio": "Naam & bio", "new_password": "Nieuw wachtwoord", "notification_visibility": "Type meldingen die getoond worden", - "notification_visibility_follows": "Volgingen", - "notification_visibility_likes": "Vind-ik-leuks", + "notification_visibility_follows": "Gevolgden", + "notification_visibility_likes": "Favorieten", "notification_visibility_mentions": "Vermeldingen", "notification_visibility_repeats": "Herhalingen", "no_rich_text_description": "Verwijder rich text formattering van alle berichten", "hide_network_description": "Toon niet wie mij volgt en wie ik volg.", - "nsfw_clickthrough": "Doorklikbaar verbergen van gevoelige bijlages inschakelen", + "nsfw_clickthrough": "Doorklikbaar verbergen van gevoelige bijlages en link voorbeelden inschakelen", "oauth_tokens": "OAuth-tokens", "token": "Token", - "refresh_token": "Token Vernieuwen", + "refresh_token": "Token vernieuwen", "valid_until": "Geldig tot", "revoke_token": "Intrekken", "panelRadius": "Panelen", "pause_on_unfocused": "Streamen pauzeren wanneer de tab niet in focus is", "presets": "Presets", - "profile_background": "Profiel Achtergrond", - "profile_banner": "Profiel Banner", + "profile_background": "Profiel achtergrond", + "profile_banner": "Profiel banner", "profile_tab": "Profiel", - "radii_help": "Stel afronding van hoeken in de interface in (in pixels)", + "radii_help": "Afronding van hoeken in de interface instellen (in pixels)", "replies_in_timeline": "Antwoorden in tijdlijn", "reply_visibility_all": "Alle antwoorden tonen", "reply_visibility_following": "Enkel antwoorden tonen die aan mij of gevolgde gebruikers gericht zijn", @@ -222,13 +242,13 @@ "saving_err": "Fout tijdens opslaan van instellingen", "saving_ok": "Instellingen opgeslagen", "security_tab": "Beveiliging", - "scope_copy": "Neem bereik over bij beantwoorden (Directe Berichten blijven altijd Direct)", + "scope_copy": "Bereik overnemen bij beantwoorden (Privéberichten blijven altijd privé)", "set_new_avatar": "Nieuwe avatar instellen", "set_new_profile_background": "Nieuwe profiel achtergrond instellen", "set_new_profile_banner": "Nieuwe profiel banner instellen", "settings": "Instellingen", "subject_input_always_show": "Altijd onderwerpveld tonen", - "subject_line_behavior": "Onderwerp kopiëren bij antwoorden", + "subject_line_behavior": "Onderwerp kopiëren bij beantwoorden", "subject_line_email": "Zoals email: \"re: onderwerp\"", "subject_line_mastodon": "Zoals mastodon: kopieer zoals het is", "subject_line_noop": "Niet kopiëren", @@ -236,7 +256,7 @@ "streaming": "Automatisch streamen van nieuwe berichten inschakelen wanneer tot boven gescrold is", "text": "Tekst", "theme": "Thema", - "theme_help": "Gebruik hex color codes (#rrggbb) om je kleurschema te wijzigen.", + "theme_help": "Hex kleur codes (#rrggbb) gebruiken om je kleur thema te wijzigen.", "theme_help_v2_1": "Je kan ook de kleur en transparantie van bepaalde componenten overschrijven door de checkbox aan te vinken, gebruik de \"Alles wissen\" knop om alle overschrijvingen te annuleren.", "theme_help_v2_2": "Iconen onder sommige onderdelen zijn achtergrond/tekst contrast indicatoren, zweef er over voor gedetailleerde info. Hou er rekening mee dat bij doorzichtigheid de ergst mogelijke situatie wordt weer gegeven.", "tooltipRadius": "Tooltips/alarmen", @@ -323,7 +343,13 @@ "popover": "Tooltips, menu's, popovers", "post": "Berichten / Gebruiker bios", "alert_neutral": "Neutraal", - "alert_warning": "Waarschuwing" + "alert_warning": "Waarschuwing", + "chat": { + "border": "Rand", + "outgoing": "Uitgaand", + "incoming": "Binnenkomend" + }, + "wallpaper": "Achtergrond" }, "radii": { "_tab_label": "Rondheid" @@ -399,50 +425,50 @@ "setup_otp": "OTP instellen", "wait_pre_setup_otp": "OTP voorinstellen", "confirm_and_enable": "Bevestig en schakel OTP in", - "title": "Twee-factor Authenticatie", + "title": "Twee-factorauthenticatie", "generate_new_recovery_codes": "Genereer nieuwe herstelcodes", "recovery_codes": "Herstelcodes.", - "waiting_a_recovery_codes": "Backup codes ontvangen…", - "authentication_methods": "Authenticatie methodes", + "waiting_a_recovery_codes": "Back-upcodes ontvangen…", + "authentication_methods": "Authenticatiemethodes", "scan": { "title": "Scannen", - "desc": "Scan de QR code of voer een sleutel in met je twee-factor applicatie:", + "desc": "Scan de QR-code of voer een sleutel in met je twee-factorapplicatie:", "secret_code": "Sleutel" }, "verify": { - "desc": "Voer de code van je twee-factor applicatie in om twee-factor authenticatie in te schakelen:" + "desc": "Voer de code van je twee-factorapplicatie in om twee-factorauthenticatie in te schakelen:" }, - "warning_of_generate_new_codes": "Wanneer je nieuwe herstelcodes genereert, zullen je oude code niet langer werken.", - "recovery_codes_warning": "Schrijf de codes op of sla ze op een veilige locatie op - anders kun je ze niet meer inzien. Als je toegang tot je 2FA app en herstelcodes verliest, zal je buitengesloten zijn uit je account." + "warning_of_generate_new_codes": "Wanneer je nieuwe herstelcodes genereert, zullen je oude codes niet langer werken.", + "recovery_codes_warning": "Schrijf de codes op of sla ze op een veilige locatie op - anders kun je ze niet meer inzien. Als je toegang tot je 2FA-app en herstelcodes verliest, zal je buitengesloten zijn van je account." }, "allow_following_move": "Automatisch volgen toestaan wanneer een gevolgd account migreert", "block_export": "Blokkades exporteren", "block_import": "Blokkades importeren", "blocks_imported": "Blokkades geïmporteerd! Het kan even duren voordat deze verwerkt zijn.", "blocks_tab": "Blokkades", - "change_email": "Email wijzigen", - "change_email_error": "Er is een fout opgetreden tijdens het wijzigen van je email.", - "changed_email": "Email succesvol gewijzigd!", + "change_email": "E-mail wijzigen", + "change_email_error": "Er is een fout opgetreden tijdens het wijzigen van je e-mailadres.", + "changed_email": "E-mailadres succesvol gewijzigd!", "domain_mutes": "Domeinen", - "avatar_size_instruction": "De aangeraden minimale afmeting voor avatar afbeeldingen is 150x150 pixels.", + "avatar_size_instruction": "De aangeraden minimale afmeting voor avatar-afbeeldingen is 150x150 pixels.", "pad_emoji": "Vul emoji aan met spaties wanneer deze met de picker ingevoegd worden", - "emoji_reactions_on_timeline": "Toon emoji reacties op de tijdlijn", + "emoji_reactions_on_timeline": "Toon emoji-reacties op de tijdlijn", "accent": "Accent", - "hide_muted_posts": "Verberg berichten van genegeerde gebruikers", + "hide_muted_posts": "Berichten van genegeerde gebruikers verbergen", "max_thumbnails": "Maximaal aantal miniaturen per bericht", - "use_one_click_nsfw": "Open gevoelige bijlagen met slechts één klik", + "use_one_click_nsfw": "Gevoelige bijlagen met slechts één klik openen", "hide_filtered_statuses": "Gefilterde statussen verbergen", - "import_blocks_from_a_csv_file": "Importeer blokkades van een csv bestand", - "mutes_tab": "Negeringen", - "play_videos_in_modal": "Speel video's af in een popup frame", - "new_email": "Nieuwe Email", + "import_blocks_from_a_csv_file": "Blokkades van een csv bestand importeren", + "mutes_tab": "Genegeerden", + "play_videos_in_modal": "Video's in een popup frame afspelen", + "new_email": "Nieuwe e-mail", "notification_visibility_emoji_reactions": "Reacties", "no_blocks": "Geen blokkades", - "no_mutes": "Geen negeringen", + "no_mutes": "Geen genegeerden", "hide_followers_description": "Niet tonen wie mij volgt", "hide_followers_count_description": "Niet mijn volgers aantal tonen", "hide_follows_count_description": "Niet mijn gevolgde aantal tonen", - "show_admin_badge": "Beheerders badge tonen in mijn profiel", + "show_admin_badge": "\"Beheerder\" badge in mijn profiel tonen", "autohide_floating_post_button": "Nieuw Bericht knop automatisch verbergen (mobiel)", "search_user_to_block": "Zoek wie je wilt blokkeren", "search_user_to_mute": "Zoek wie je wilt negeren", @@ -452,31 +478,69 @@ "useStreamingApi": "Berichten en meldingen in real-time ontvangen", "useStreamingApiWarning": "(Afgeraden, experimenteel, kan berichten overslaan)", "type_domains_to_mute": "Zoek domeinen om te negeren", - "upload_a_photo": "Upload een foto", + "upload_a_photo": "Foto uploaden", "fun": "Plezier", "greentext": "Meme pijlen", - "block_export_button": "Exporteer je geblokkeerde gebruikers naar een csv bestand", + "block_export_button": "Exporteer je geblokkeerde gebruikers naar een csv-bestand", "block_import_error": "Fout bij importeren blokkades", "discoverable": "Sta toe dat dit account ontdekt kan worden in zoekresultaten en andere diensten", - "use_contain_fit": "Snij bijlage in miniaturen niet bij", + "use_contain_fit": "Bijlage in miniaturen niet bijsnijden", "notification_visibility_moves": "Gebruiker Migraties", "hide_follows_description": "Niet tonen wie ik volg", - "show_moderator_badge": "Moderators badge tonen in mijn profiel", + "show_moderator_badge": "\"Moderator\" badge in mijn profiel tonen", "notification_setting_filters": "Filters", "notification_blocks": "Door een gebruiker te blokkeren, ontvang je geen meldingen meer van de gebruiker en wordt je abonnement op de gebruiker opgeheven.", "version": { - "frontend_version": "Frontend Versie", - "backend_version": "Backend Versie", + "frontend_version": "Frontend versie", + "backend_version": "Backend versie", "title": "Versie" }, "mutes_and_blocks": "Negeringen en Blokkades", "profile_fields": { "value": "Inhoud", "name": "Label", - "add_field": "Veld Toevoegen", + "add_field": "Veld toevoegen", "label": "Profiel metadata" }, - "bot": "Dit is een bot account" + "bot": "Dit is een bot-account", + "setting_changed": "Instelling verschilt van standaard waarde", + "save": "Wijzigingen opslaan", + "hide_media_previews": "Media voorbeelden verbergen", + "word_filter": "Woord filter", + "chatMessageRadius": "Chatbericht", + "mute_export": "Genegeerden export", + "mute_export_button": "Exporteer je genegeerden naar een csv-bestand", + "mute_import_error": "Fout tijdens het importeren van genegeerden", + "mute_import": "Genegeerden import", + "mutes_imported": "Genegeerden geïmporteerd! Het kan even duren voordat deze verwerkt zijn.", + "more_settings": "Meer instellingen", + "notification_setting_hide_notification_contents": "Afzender en inhoud van push meldingen verbergen", + "notification_setting_block_from_strangers": "Meldingen van gebruikers die je niet volgt blokkeren", + "virtual_scrolling": "Tijdlijn rendering optimaliseren", + "sensitive_by_default": "Berichten standaard als gevoelig markeren", + "reset_avatar_confirm": "Wil je echt de avatar herstellen?", + "reset_banner_confirm": "Wil je echt de banner herstellen?", + "reset_background_confirm": "Wil je echt de achtergrond herstellen?", + "reset_profile_banner": "Profiel banner herstellen", + "reset_profile_background": "Profiel achtergrond herstellen", + "reset_avatar": "Avatar herstellen", + "reply_visibility_self_short": "Alleen antwoorden aan mijzelf tonen", + "reply_visibility_following_short": "Antwoorden naar mijn gevolgden tonen", + "file_export_import": { + "errors": { + "file_slightly_new": "Bestand minor versie is verschillend, sommige instellingen kunnen mogelijk niet worden geladen", + "file_too_old": "Incompatibele hoofdversie: {fileMajor}, bestandsversie is te oud en wordt niet ondersteund (minimale versie {feMajor})", + "file_too_new": "Incompatibele hoofdversie: {fileMajor}, deze PleromaFE (instellingen versie {feMajor}) is te oud om deze te ondersteunen", + "invalid_file": "Het geselecteerde bestand is niet een door Pleroma ondersteunde instellingen back-up. Er zijn geen wijzigingen gemaakt." + }, + "restore_settings": "Instellingen uit bestand herstellen", + "backup_settings_theme": "Instellingen en thema naar bestand back-uppen", + "backup_settings": "Instellingen naar bestand back-uppen", + "backup_restore": "Instellingen backup" + }, + "hide_wallpaper": "Instantie achtergrond verbergen", + "hide_all_muted_posts": "Genegeerde berichten verbergen", + "import_mutes_from_a_csv_file": "Importeer genegeerden van een csv bestand" }, "timeline": { "collapse": "Inklappen", @@ -488,7 +552,11 @@ "show_new": "Nieuwe tonen", "up_to_date": "Up-to-date", "no_statuses": "Geen statussen", - "no_more_statuses": "Geen statussen meer" + "no_more_statuses": "Geen statussen meer", + "socket_broke": "Realtime verbinding verloren: CloseEvent code {0}", + "socket_reconnected": "Realtime verbinding opgezet", + "reload": "Verversen", + "error": "Fout tijdens het ophalen van tijdlijn: {0}" }, "user_card": { "approve": "Goedkeuren", @@ -497,9 +565,9 @@ "deny": "Weigeren", "favorites": "Favorieten", "follow": "Volgen", + "follow_cancel": "Aanvraag annuleren", "follow_sent": "Aanvraag verzonden!", "follow_progress": "Aanvragen…", - "follow_again": "Aanvraag opnieuw zenden?", "follow_unfollow": "Stop volgen", "followees": "Aan het volgen", "followers": "Volgers", @@ -543,10 +611,18 @@ "report": "Aangeven", "mention": "Vermelding", "media": "Media", - "hidden": "Verborgen" + "hidden": "Verborgen", + "highlight": { + "side": "Zijstreep", + "striped": "Gestreepte achtergrond", + "solid": "Effen achtergrond", + "disabled": "Geen highlight" + }, + "bot": "Bot", + "message": "Bericht" }, "user_profile": { - "timeline_title": "Gebruikers Tijdlijn", + "timeline_title": "Gebruikerstijdlijn", "profile_loading_error": "Sorry, er is een fout opgetreden bij het laden van dit profiel.", "profile_does_not_exist": "Sorry, dit profiel bestaat niet." }, @@ -555,20 +631,22 @@ "who_to_follow": "Wie te volgen" }, "tool_tip": { - "media_upload": "Media Uploaden", + "media_upload": "Media uploaden", "repeat": "Herhalen", "reply": "Beantwoorden", "favorite": "Favoriet maken", "user_settings": "Gebruikers Instellingen", "reject_follow_request": "Volg-verzoek afwijzen", "accept_follow_request": "Volg-aanvraag accepteren", - "add_reaction": "Reactie toevoegen" + "add_reaction": "Reactie toevoegen", + "bookmark": "Bladwijzer" }, "upload": { "error": { "base": "Upload mislukt.", "file_too_big": "Bestand is te groot [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", - "default": "Probeer het later opnieuw" + "default": "Probeer het later opnieuw", + "message": "Upload is mislukt: {0}" }, "file_size_units": { "B": "B", @@ -585,25 +663,28 @@ "reject": "Afwijzen", "replace": "Vervangen", "is_replaced_by": "→", - "keyword_policies": "Zoekwoord Beleid", + "keyword_policies": "Zoekwoordbeleid", "ftl_removal": "Verwijdering van \"Het Geheel Bekende Netwerk\" Tijdlijn" }, - "mrf_policies_desc": "MRF regels beïnvloeden het federatie gedrag van de instantie. De volgende regels zijn ingeschakeld:", - "mrf_policies": "Ingeschakelde MRF Regels", + "mrf_policies_desc": "MRF-regels beïnvloeden het federatiegedrag van de instantie. De volgende regels zijn ingeschakeld:", + "mrf_policies": "Ingeschakelde MRF-regels", "simple": { - "simple_policies": "Instantie-specifieke Regels", + "simple_policies": "Instantiespecifieke regels", + "instance": "Instantie", + "reason": "Reden", + "not_applicable": "n.v.t.", "accept": "Accepteren", "accept_desc": "Deze instantie accepteert alleen berichten van de volgende instanties:", "reject": "Afwijzen", "reject_desc": "Deze instantie zal geen berichten accepteren van de volgende instanties:", "quarantine": "Quarantaine", - "quarantine_desc": "Deze instantie zal alleen publieke berichten sturen naar de volgende instanties:", - "ftl_removal_desc": "Deze instantie verwijdert de volgende instanties van \"Het Geheel Bekende Netwerk\" tijdlijn:", + "quarantine_desc": "Deze instantie zal alleen openbare berichten sturen naar de volgende instanties:", + "ftl_removal_desc": "Deze instantie verwijdert de volgende instanties van \"Bekende Netwerk\" tijdlijn:", "media_removal_desc": "Deze instantie verwijdert media van berichten van de volgende instanties:", "media_nsfw_desc": "Deze instantie stelt media in als gevoelig in berichten van de volgende instanties:", - "ftl_removal": "Verwijderen van \"Het Geheel Bekende Netwerk\" Tijdlijn", - "media_removal": "Media Verwijdering", - "media_nsfw": "Forceer Media als Gevoelig" + "ftl_removal": "Verwijderen van \"Bekende Netwerk\" Tijdlijn", + "media_removal": "Mediaverwijdering", + "media_nsfw": "Forceer media als gevoelig" } }, "staff": "Personeel" @@ -634,8 +715,8 @@ "next": "Volgende" }, "polls": { - "add_poll": "Poll Toevoegen", - "add_option": "Optie Toevoegen", + "add_poll": "Poll toevoegen", + "add_option": "Optie toevoegen", "option": "Optie", "votes": "stemmen", "vote": "Stem", @@ -645,31 +726,33 @@ "expires_in": "Poll eindigt in {0}", "expired": "Poll is {0} geleden beëindigd", "not_enough_options": "Te weinig opties in poll", - "type": "Poll type" + "type": "Poll-type", + "votes_count": "{count} stem | {count} stemmen", + "people_voted_count": "{count} persoon heeft gestemd | {count} personen hebben gestemd" }, "emoji": { "emoji": "Emoji", "keep_open": "Picker openhouden", - "search_emoji": "Zoek voor een emoji", + "search_emoji": "Emoji zoeken", "add_emoji": "Emoji invoegen", - "unicode": "Unicode emoji", + "unicode": "Unicode-emoji", "load_all": "Alle {emojiAmount} emoji worden geladen", "stickers": "Stickers", "load_all_hint": "Eerste {saneAmount} emoji geladen, alle emoji tegelijk laden kan problemen veroorzaken met prestaties.", "custom": "Gepersonaliseerde emoji" }, "interactions": { - "favs_repeats": "Herhalingen en Favorieten", - "follows": "Nieuwe volgingen", + "favs_repeats": "Herhalingen en favorieten", + "follows": "Nieuwe gevolgden", + "moves": "Gebruikermigraties", "emoji_reactions": "Emoji Reacties", "reports": "Rapportages", - "moves": "Gebruiker migreert", "load_older": "Oudere interacties laden" }, "remote_user_resolver": { "searching_for": "Zoeken naar", "error": "Niet gevonden.", - "remote_user_resolver": "Externe gebruikers zoeker" + "remote_user_resolver": "Externe gebruikers-zoeker" }, "report": { "reporter": "Reporteerder:", @@ -728,7 +811,17 @@ "repeats": "Herhalingen", "favorites": "Favorieten", "thread_muted_and_words": ", heeft woorden:", - "thread_muted": "Thread genegeerd" + "thread_muted": "Thread genegeerd", + "expand": "Uitklappen", + "nsfw": "Gevoelig", + "status_deleted": "Dit bericht is verwijderd", + "hide_content": "Inhoud verbergen", + "show_content": "Inhoud tonen", + "hide_full_subject": "Volledig onderwerp verbergen", + "show_full_subject": "Volledig onderwerp tonen", + "external_source": "Externe bron", + "unbookmark": "Bladwijzer verwijderen", + "bookmark": "Bladwijzer toevoegen" }, "time": { "years_short": "{0}j", @@ -763,5 +856,33 @@ "day_short": "{0}d", "days": "{0} dagen", "day": "{0} dag" + }, + "shoutbox": { + "title": "Shoutbox" + }, + "errors": { + "storage_unavailable": "Pleroma kon browseropslag niet benaderen. Je login of lokale instellingen worden niet opgeslagen en je kunt onverwachte problemen ondervinden. Probeer cookies te accepteren." + }, + "display_date": { + "today": "Vandaag" + }, + "file_type": { + "file": "Bestand", + "image": "Afbeelding", + "video": "Video", + "audio": "Audio" + }, + "chats": { + "empty_chat_list_placeholder": "Je hebt nog geen chats. Start een nieuwe chat!", + "error_sending_message": "Er is iets fout gegaan tijdens het verzenden van het bericht.", + "error_loading_chat": "Er is iets fout gegaan tijdens het laden van de chat.", + "delete_confirm": "Wil je echt dit bericht verwijderen?", + "more": "Meer", + "empty_message_error": "Kan niet een leeg bericht plaatsen", + "new": "Nieuwe Chat", + "chats": "Chats", + "delete": "Verwijderen", + "message_user": "Spreek met {nickname}", + "you": "Jij:" } } diff --git a/src/i18n/oc.json b/src/i18n/oc.json index 24001d4aae3399beb6fc235ead7487cbe66dd6d5..40f48149e2d07c28feca86d53f37b562cff50b2b 100644 --- a/src/i18n/oc.json +++ b/src/i18n/oc.json @@ -465,7 +465,6 @@ "follow": "Seguir", "follow_sent": "Demanda enviada !", "follow_progress": "Demanda…", - "follow_again": "Tornar enviar la demanda ?", "follow_unfollow": "Quitar de seguir", "followees": "Abonaments", "followers": "Seguidors", diff --git a/src/i18n/pl.json b/src/i18n/pl.json index 7cf067965c516540deb456a8ffe33e1449936b2b..304a03491e99938415f0d550d656ed3d406d22a6 100644 --- a/src/i18n/pl.json +++ b/src/i18n/pl.json @@ -19,8 +19,8 @@ "reject_desc": "Ta instancja odrzuca posty z wymienionych instancji:", "quarantine": "Kwarantanna", "quarantine_desc": "Ta instancja wysyÅ‚a tylko publiczne posty do wymienionych instancji:", - "ftl_removal": "UsuniÄ™cie z \"CaÅ‚ej znanej sieci\"", - "ftl_removal_desc": "Ta instancja usuwa wymienionych instancje z \"CaÅ‚ej znanej sieci\":", + "ftl_removal": "UsuniÄ™cie z „CaÅ‚ej znanej sieciâ€", + "ftl_removal_desc": "Ta instancja usuwa wymienionych instancje z „CaÅ‚ej znanej sieciâ€:", "media_removal": "Usuwanie multimediów", "media_removal_desc": "Ta instancja usuwa multimedia z postów od wymienionych instancji:", "media_nsfw": "Multimedia ustawione jako wrażliwe", @@ -75,7 +75,13 @@ "loading": "Åadowanie…", "retry": "Spróbuj ponownie", "peek": "Spójrz", - "error_retry": "Spróbuj ponownie" + "error_retry": "Spróbuj ponownie", + "flash_content": "NaciÅ›nij, aby wyÅ›wietlić zawartoÅ›ci Flash z użyciem Ruffle (eksperymentalnie, może nie dziaÅ‚ać).", + "flash_fail": "Nie udaÅ‚o siÄ™ zaÅ‚adować treÅ›ci flash, zajrzyj do konsoli, aby odnaleźć szczegóły.", + "role": { + "moderator": "Moderator", + "admin": "Administrator" + } }, "image_cropper": { "crop_picture": "Przytnij obrazek", @@ -118,7 +124,7 @@ "friend_requests": "ProÅ›by o możliwość obserwacji", "mentions": "Wzmianki", "interactions": "Interakcje", - "dms": "WiadomoÅ›ci prywatne", + "dms": "WiadomoÅ›ci bezpoÅ›rednie", "public_tl": "Publiczna oÅ› czasu", "timeline": "OÅ› czasu", "twkn": "Znana sieć", @@ -128,7 +134,8 @@ "preferences": "Preferencje", "bookmarks": "ZakÅ‚adki", "chats": "Czaty", - "timelines": "Osie czasu" + "timelines": "Osie czasu", + "home_timeline": "Główna oÅ› czasu" }, "notifications": { "broken_favorite": "Nieznany status, szukam go…", @@ -156,7 +163,9 @@ "expiry": "Czas trwania ankiety", "expires_in": "Ankieta koÅ„czy siÄ™ za {0}", "expired": "Ankieta skoÅ„czyÅ‚a siÄ™ {0} temu", - "not_enough_options": "Zbyt maÅ‚o unikalnych opcji w ankiecie" + "not_enough_options": "Zbyt maÅ‚o unikalnych opcji w ankiecie", + "people_voted_count": "{count} osoba zagÅ‚osowaÅ‚a | {count} osoby zagÅ‚osowaÅ‚y | {count} osób zagÅ‚osowaÅ‚o", + "votes_count": "{count} gÅ‚os | {count} gÅ‚osy | {count} gÅ‚osów" }, "emoji": { "stickers": "Naklejki", @@ -197,16 +206,17 @@ "unlisted": "Ten post nie bÄ™dzie widoczny na publicznej osi czasu i caÅ‚ej znanej sieci" }, "scope": { - "direct": "BezpoÅ›redni – Tylko dla wspomnianych użytkowników", - "private": "Tylko dla obserwujÄ…cych – Umieść dla osób, które ciÄ™ obserwujÄ…", - "public": "Publiczny – Umieść na publicznych osiach czasu", - "unlisted": "Niewidoczny – Nie umieszczaj na publicznych osiach czasu" + "direct": "BezpoÅ›redni – tylko dla wspomnianych użytkowników", + "private": "Tylko dla obserwujÄ…cych – umieść dla osób, które ciÄ™ obserwujÄ…", + "public": "Publiczny – umieść na publicznych osiach czasu", + "unlisted": "Niewidoczny – nie umieszczaj na publicznych osiach czasu" }, "preview_empty": "Pusty", "preview": "PodglÄ…d", "empty_status_error": "Nie można wysÅ‚ać pustego wpisu bez plików", "media_description_error": "Nie udaÅ‚o siÄ™ zaktualizować mediów, spróbuj ponownie", - "media_description": "Opis mediów" + "media_description": "Opis mediów", + "post": "Opublikuj" }, "registration": { "bio": "Bio", @@ -227,7 +237,10 @@ "password_required": "nie może być puste", "password_confirmation_required": "nie może być puste", "password_confirmation_match": "musi być takie jak hasÅ‚o" - } + }, + "reason": "Powód rejestracji", + "reason_placeholder": "Ta instancja rÄ™cznie zatwierdza rejestracje.\nPoinformuj administratora, dlaczego chcesz siÄ™ zarejestrować.", + "register": "Zarejestruj siÄ™" }, "remote_user_resolver": { "remote_user_resolver": "Wyszukiwarka użytkowników nietutejszych", @@ -281,7 +294,7 @@ "cGreen": "Zielony (powtórzenia)", "cOrange": "PomaraÅ„czowy (ulubione)", "cRed": "Czerwony (anuluj)", - "change_email": "ZmieÅ„ email", + "change_email": "ZmieÅ„ e-mail", "change_email_error": "WystÄ…piÅ‚ problem podczas zmiany emaila.", "changed_email": "PomyÅ›lnie zmieniono email!", "change_password": "ZmieÅ„ hasÅ‚o", @@ -345,7 +358,7 @@ "use_contain_fit": "Nie przycinaj zaÅ‚Ä…czników na miniaturach", "name": "ImiÄ™", "name_bio": "ImiÄ™ i bio", - "new_email": "Nowy email", + "new_email": "Nowy e-mail", "new_password": "Nowe hasÅ‚o", "notification_visibility": "Rodzaje powiadomieÅ„ do wyÅ›wietlania", "notification_visibility_follows": "Obserwacje", @@ -361,8 +374,8 @@ "hide_followers_description": "Nie pokazuj kto mnie obserwuje", "hide_follows_count_description": "Nie pokazuj licznika obserwowanych", "hide_followers_count_description": "Nie pokazuj licznika obserwujÄ…cych", - "show_admin_badge": "Pokazuj odznakÄ™ Administrator na moim profilu", - "show_moderator_badge": "Pokazuj odznakÄ™ Moderator na moim profilu", + "show_admin_badge": "Pokazuj odznakÄ™ „Administrator†na moim profilu", + "show_moderator_badge": "Pokazuj odznakÄ™ „Moderator†na moim profilu", "nsfw_clickthrough": "WÅ‚Ä…cz domyÅ›lne ukrywanie zaÅ‚Ä…czników o treÅ›ci nieprzyzwoitej (NSFW)", "oauth_tokens": "Tokeny OAuth", "token": "Token", @@ -600,7 +613,27 @@ "mute_import": "Import wyciszeÅ„", "mute_export_button": "Wyeksportuj swoje wyciszenia do pliku .csv", "mute_export": "Eksport wyciszeÅ„", - "hide_wallpaper": "Ukryj tÅ‚o instancji" + "hide_wallpaper": "Ukryj tÅ‚o instancji", + "save": "Zapisz zmiany", + "setting_changed": "Opcja różni siÄ™ od domyÅ›lnej", + "right_sidebar": "Pokaż pasek boczny po prawej", + "file_export_import": { + "errors": { + "invalid_file": "Wybrany plik nie jest obsÅ‚ugiwanÄ… kopiÄ… zapasowÄ… ustawieÅ„ Pleromy. Nie dokonano żadnych zmian." + }, + "backup_restore": "Kopia zapasowa ustawieÅ„", + "backup_settings": "Kopia zapasowa ustawieÅ„ do pliku", + "backup_settings_theme": "Kopia zapasowa ustawieÅ„ i motywu do pliku", + "restore_settings": "Przywróć ustawienia z pliku" + }, + "more_settings": "WiÄ™cej ustawieÅ„", + "word_filter": "Filtr słów", + "hide_media_previews": "Ukryj podglÄ…d mediów", + "hide_all_muted_posts": "Ukryj wyciszone sÅ‚owa", + "reply_visibility_following_short": "Pokazuj odpowiedzi obserwujÄ…cym", + "reply_visibility_self_short": "Pokazuj odpowiedzi tylko do mnie", + "sensitive_by_default": "DomyÅ›lnie oznaczaj wpisy jako wrażliwe", + "hide_shoutbox": "Ukryj shoutbox instancji" }, "time": { "day": "{0} dzieÅ„", @@ -648,7 +681,9 @@ "no_more_statuses": "Brak kolejnych statusów", "no_statuses": "Brak statusów", "reload": "OdÅ›wież", - "error": "BÅ‚Ä…d pobierania osi czasu: {0}" + "error": "BÅ‚Ä…d pobierania osi czasu: {0}", + "socket_broke": "Utracono poÅ‚Ä…czenie w czasie rzeczywistym: kod CloseEvent {0}", + "socket_reconnected": "OsiÄ…gniÄ™to poÅ‚Ä…czenie w czasie rzeczywistym" }, "status": { "favorites": "Ulubione", @@ -686,7 +721,6 @@ "follow": "Obserwuj", "follow_sent": "WysÅ‚ano proÅ›bÄ™!", "follow_progress": "WysyÅ‚am proÅ›bę…", - "follow_again": "WysÅ‚ać proÅ›bÄ™ ponownie?", "follow_unfollow": "PrzestaÅ„ obserwować", "followees": "Obserwowani", "followers": "ObserwujÄ…cy", @@ -731,7 +765,12 @@ "delete_user": "UsuÅ„ użytkownika", "delete_user_confirmation": "Czy jesteÅ› absolutnie pewny(-a)? Ta operacja nie może być cofniÄ™ta." }, - "message": "Napisz" + "message": "Napisz", + "edit_profile": "Edytuj profil", + "highlight": { + "disabled": "Bez wyróżnienia" + }, + "bot": "Bot" }, "user_profile": { "timeline_title": "OÅ› czasu użytkownika", diff --git a/src/i18n/pt.json b/src/i18n/pt.json index b79985e8a56565726e782ac74fe0a8852d22c4f6..e32a95e4518162b862904a40b03098df1c9c3879 100644 --- a/src/i18n/pt.json +++ b/src/i18n/pt.json @@ -35,7 +35,12 @@ "retry": "Tenta novamente", "error_retry": "Por favor, tenta novamente", "loading": "A carregar…", - "dismiss": "Ignorar" + "dismiss": "Ignorar", + "role": + { + "moderator": "Moderador", + "admin": "Admin" + } }, "image_cropper": { "crop_picture": "Cortar imagem", @@ -570,7 +575,6 @@ "follow": "Seguir", "follow_sent": "Pedido enviado!", "follow_progress": "Enviando…", - "follow_again": "Enviar solicitação novamente?", "follow_unfollow": "Deixar de seguir", "followees": "Seguindo", "followers": "Seguidores", @@ -615,11 +619,7 @@ "report": "Denunciar", "message": "Mensagem", "mention": "Mencionar", - "hidden": "Ocultar", - "roles": { - "moderator": "Moderador", - "admin": "Admin" - } + "hidden": "Ocultar" }, "user_profile": { "timeline_title": "Cronologia do Utilizador", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 30a657414dec54d2b8fe68f247b79962b61e41d0..ba0cec28f7fce331af76d786046017913a024a59 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -13,7 +13,7 @@ "disable": "Оключить", "enable": "Включить", "confirm": "Подтвердить", - "verify": "Проверить", + "verify": "Подтверждение", "more": "Больше", "generic_error": "Произошла ошибка", "optional": "не обÑзательно", @@ -24,7 +24,11 @@ "retry": "Попробуйте еще раз", "error_retry": "ПожалуйÑта попробуйте еще раз", "close": "Закрыть", - "loading": "Загрузка…" + "loading": "Загрузка…", + "role": { + "moderator": "Модератор", + "admin": "ÐдминиÑтратор" + } }, "login": { "login": "Войти", @@ -40,8 +44,8 @@ "heading": { "TotpForm": "Ð”Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð°Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ", "RecoveryForm": "Two-factor recovery", - "totp": "Ð”Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð°Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ", - "recovery": "Двухфакторное возвращение аккаунта" + "totp": "ДвухÑÑ‚Ð°Ð¿Ð½Ð°Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ", + "recovery": "ВоÑÑтановление двухÑтапной аутентификации" }, "hint": "Войдите чтобы приÑоединитьÑÑ Ðº диÑкуÑÑии", "description": "Войти Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ OAuth" @@ -51,8 +55,8 @@ "chat": "Локальный чат", "mentions": "УпоминаниÑ", "interactions": "ВзаимодейÑтвиÑ", - "public_tl": "ÐŸÑƒÐ±Ð»Ð¸Ñ‡Ð½Ð°Ñ Ð»ÐµÐ½Ñ‚Ð°", - "timeline": "Лента", + "public_tl": "Ð›Ð¾ÐºÐ°Ð»ÑŒÐ½Ð°Ñ Ð»ÐµÐ½Ñ‚Ð°", + "timeline": "ГлавнаÑ", "twkn": "Ð¤ÐµÐ´ÐµÑ€Ð°Ñ‚Ð¸Ð²Ð½Ð°Ñ Ð»ÐµÐ½Ñ‚Ð°", "search": "ПоиÑк", "friend_requests": "ЗапроÑÑ‹ на чтение", @@ -61,10 +65,11 @@ "timelines": "Ленты", "preferences": "ÐаÑтройки", "who_to_follow": "Кого читать", - "dms": "Личные СообщениÑ", + "dms": "Личные ÑообщениÑ", "administration": "Панель админиÑтратора", - "about": "О Ñервере", - "user_search": "ПоиÑк пользователей" + "about": "Об узле", + "user_search": "ПоиÑк пользователей", + "home_timeline": "ГлавнаÑ" }, "notifications": { "broken_favorite": "ÐеизвеÑтный ÑтатуÑ, ищем…", @@ -75,35 +80,35 @@ "read": "ПрочеÑÑ‚ÑŒ", "repeated_you": "повторил(а) ваш ÑтатуÑ", "follow_request": "хочет читать ваÑ", - "reacted_with": "добавил реакцию: {0}", - "migrated_to": "мигрировал на", + "reacted_with": "добавил(а) реакцию: {0}", + "migrated_to": "перехал на", "no_more_notifications": "Ðет дальнейших уведомлений", "error": "Ошибка при обновлении уведомлений: {0}" }, "interactions": { - "favs_repeats": "Повторы и фавориты", + "favs_repeats": "Повторы и отметки «ÐравитÑÑ»", "follows": "Ðовые читатели", "load_older": "Загрузить Ñтарые взаимодейÑтвиÑ", - "moves": "Миграции пользователей" + "moves": "Переезды" }, "post_status": { - "account_not_locked_warning": "Ваш аккаунт не {0}. Кто угодно может начать читать Ð²Ð°Ñ Ñ‡Ñ‚Ð¾Ð±Ñ‹ видеть поÑÑ‚Ñ‹ только Ð´Ð»Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñчиков.", - "account_not_locked_warning_link": "залочен", - "attachments_sensitive": "Ð’Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ñодержат чувÑтвительный контент", + "account_not_locked_warning": "Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ не {0}. Кто угодно может начать читать Ð²Ð°Ñ Ñ‡Ñ‚Ð¾Ð±Ñ‹ видеть ÑтатуÑÑ‹ только Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ñ‚ÐµÐ»ÐµÐ¹.", + "account_not_locked_warning_link": "закрыт", + "attachments_sensitive": "Ð’Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÑŽÑ‚ щекотливый характер", "content_warning": "Тема (не обÑзательно)", "default": "Что нового?", "direct_warning": "Ðтот поÑÑ‚ будет виден только упомÑнутым пользователÑм", "posting": "ОтправлÑетÑÑ", "scope_notice": { - "public": "Ðтот поÑÑ‚ будет виден вÑем", - "private": "Ðтот поÑÑ‚ будет виден только вашим подпиÑчикам", - "unlisted": "Ðтот поÑÑ‚ не будет виден в публичной и федеративной ленте" + "public": "Ðтот ÑÑ‚Ð°Ñ‚ÑƒÑ Ð±ÑƒÐ´ÐµÑ‚ виден вÑем", + "private": "Ðтот ÑÑ‚Ð°Ñ‚ÑƒÑ Ð±ÑƒÐ´ÐµÑ‚ виден только вашим читателÑм", + "unlisted": "Ðтот ÑÑ‚Ð°Ñ‚ÑƒÑ Ð½Ðµ будет виден в локальной и федеративной ленте" }, "scope": { - "direct": "Личное - Ñтот поÑÑ‚ видÑÑ‚ только те кто в нём упомÑнут", - "private": "Ð”Ð»Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñчиков - Ñтот поÑÑ‚ видÑÑ‚ только подпиÑчики", - "public": "Публичный - Ñтот поÑÑ‚ виден вÑем", - "unlisted": "Ðепубличный - Ñтот поÑÑ‚ не виден на публичных лентах" + "direct": "Личное Ñообщение - Ñтот ÑÑ‚Ð°Ñ‚ÑƒÑ Ð²Ð¸Ð´ÑÑ‚ только те, кто в нём упомÑнут", + "private": "Ð”Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ñ‚ÐµÐ»ÐµÐ¹ - Ñтот ÑÑ‚Ð°Ñ‚ÑƒÑ Ð²Ð¸Ð´ÑÑ‚ только ваши читатели", + "public": "Публичный - Ñтот ÑÑ‚Ð°Ñ‚ÑƒÑ Ð²Ð¸Ð´ÐµÐ½ вÑем", + "unlisted": "Тихий - Ñтот поÑÑ‚ виден вÑем, но не отображаетÑÑ Ð² публичных лентах" }, "preview_empty": "ПуÑтой предпроÑмотр", "media_description_error": "Ðе удалоÑÑŒ обновить вложение, попробуйте еще раз", @@ -118,11 +123,12 @@ "text/plain": "ПроÑтой текÑÑ‚" }, "media_description": "ОпиÑание вложениÑ", - "new_status": "ÐапиÑать новый ÑтатуÑ" + "new_status": "ÐапиÑать новый ÑтатуÑ", + "post": "Опубликовать" }, "registration": { - "bio": "ОпиÑание", - "email": "Email", + "bio": "О Ñебе", + "email": "ÐÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð°", "fullname": "Отображаемое имÑ", "password_confirm": "Подтверждение паролÑ", "registration": "РегиÑтрациÑ", @@ -139,7 +145,10 @@ "fullname_placeholder": "например: Почтальон Печкин", "username_placeholder": "например: pechkin", "captcha": "Код подтверждениÑ", - "new_captcha": "Ðажмите на изображение чтобы получить новый код" + "new_captcha": "Ðажмите на изображение чтобы получить новый код", + "reason_placeholder": "Данный узел обрабатывает запроÑÑ‹ на региÑтрацию вручную.\nРаÑÑкажите админиÑтрации почему вы хотите зарегиÑтрироватьÑÑ.", + "reason": "Причина региÑтрации", + "register": "ЗарегиÑтрироватьÑÑ" }, "settings": { "enter_current_password_to_confirm": "Введите Ñвой текущий пароль", @@ -148,7 +157,7 @@ "setup_otp": "ÐаÑтройка OTP", "wait_pre_setup_otp": "Ð¿Ñ€ÐµÐ´Ð²Ð°Ñ€Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð½Ð°Ñтройка OTP", "confirm_and_enable": "Подтвердить и включить OTP", - "title": "Ð”Ð²ÑƒÑ…Ñ„Ð°ÐºÑ‚Ð¾Ñ€Ð½Ð°Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ", + "title": "ДвухÑÑ‚Ð°Ð¿Ð½Ð°Ñ Ð°ÑƒÑ‚ÐµÐ½Ñ‚Ð¸Ñ„Ð¸ÐºÐ°Ñ†Ð¸Ñ", "generate_new_recovery_codes": "Получить новые коды воÑтановлениÑ", "warning_of_generate_new_codes": "ПоÑле Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð½Ð¾Ð²Ñ‹Ñ… кодов воÑÑтановлениÑ, Ñтарые больше не будут работать.", "recovery_codes": "Коды воÑÑтановлениÑ.", @@ -157,11 +166,11 @@ "authentication_methods": "Методы аутентификации", "scan": { "title": "Сканирование", - "desc": "ИÑпользуйте приложение Ð´Ð»Ñ Ð´Ð²ÑƒÑ…Ñтапной аутентификации Ð´Ð»Ñ ÑÐºÐ°Ð½Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñтого QR-код или введите текÑтовый ключ:", + "desc": "ОтÑканируйте QR-код приложением Ð´Ð»Ñ Ð´Ð²ÑƒÑ…Ñтапной аутентификации или введите текÑтовый ключ:", "secret_code": "Ключ" }, "verify": { - "desc": "Чтобы включить двухÑтапную аутентификации, введите код из вашего приложение Ð´Ð»Ñ Ð´Ð²ÑƒÑ…Ñтапной аутентификации:" + "desc": "Чтобы включить двухÑтапную аутентификацию, введите код из приложениÑ-аутентификатора:" } }, "attachmentRadius": "Прикреплённые файлы", @@ -170,16 +179,16 @@ "avatarAltRadius": "Ðватары в уведомлениÑÑ…", "avatarRadius": "Ðватары", "background": "Фон", - "bio": "ОпиÑание", + "bio": "О Ñебе", "btnRadius": "Кнопки", - "bot": "Ðто аккаунт бота", + "bot": "Ðто ÑƒÑ‡Ñ‘Ñ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ бота", "cBlue": "Ответить, читать", "cGreen": "Повторить", "cOrange": "ÐравитÑÑ", "cRed": "Отменить", - "change_email": "Сменить email", - "change_email_error": "Произошла ошибка при попытке изменить email.", - "changed_email": "Email изменён уÑпешно!", + "change_email": "Сменить Ð°Ð´Ñ€ÐµÑ Ñлектронной почты", + "change_email_error": "Произошла ошибка при попытке изменить Ñлектронную почту.", + "changed_email": "ÐÐ»ÐµÐºÑ‚Ñ€Ð¾Ð½Ð½Ð°Ñ Ð¿Ð¾Ñ‡Ñ‚Ð° изменена уÑпешно!", "change_password": "Сменить пароль", "change_password_error": "Произошла ошибка при попытке изменить пароль.", "changed_password": "Пароль изменён уÑпешно!", @@ -189,9 +198,9 @@ "current_password": "Текущий пароль", "current_profile_banner": "Текущий баннер профилÑ", "data_import_export_tab": "Импорт / ÐкÑпорт данных", - "delete_account": "Удалить аккаунт", - "delete_account_description": "Удалить вашу учётную запиÑÑŒ и вÑе ваши ÑообщениÑ.", - "delete_account_error": "Возникла ошибка в процеÑÑе ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð²Ð°ÑˆÐµÐ³Ð¾ аккаунта. ЕÑли Ñто повторÑетÑÑ, ÑвÑжитеÑÑŒ Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратором вашего Ñервера.", + "delete_account": "Удалить учетную запиÑÑŒ", + "delete_account_description": "ÐавÑегда удалить вашу учётную запиÑÑŒ и ваши ÑтатуÑÑ‹.", + "delete_account_error": "Возникла ошибка в процеÑÑе ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ Ð²Ð°ÑˆÐµÐ¹ учетной запиÑи. ЕÑли Ñто повторÑетÑÑ, ÑвÑжитеÑÑŒ Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратором данного узла.", "delete_account_instructions": "Введите ваш пароль в поле ниже Ð´Ð»Ñ Ð¿Ð¾Ð´Ñ‚Ð²ÐµÑ€Ð¶Ð´ÐµÐ½Ð¸Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð¸Ñ.", "export_theme": "Сохранить Тему", "filtering": "ФильтрациÑ", @@ -217,28 +226,28 @@ "interfaceLanguage": "Язык интерфейÑа", "limited_availability": "Ðе доÑтупно в вашем браузере", "links": "СÑылки", - "lock_account_description": "Ðккаунт доÑтупен только подтверждённым подпиÑчикам", + "lock_account_description": "Сделать учетную запиÑÑŒ закрытой — подтверждать читателей вручную", "loop_video": "Зациливать видео", "loop_video_silent_only": "Зацикливать только беззвучные видео (Ñ‚.е. \"гифки\" Ñ Mastodon)", "name": "ИмÑ", - "name_bio": "Ð˜Ð¼Ñ Ð¸ опиÑание", - "new_email": "Ðовый email", + "name_bio": "Личные данные", + "new_email": "Ðовый Ð°Ð´Ñ€ÐµÑ Ñлектронной почты", "new_password": "Ðовый пароль", "fun": "Потешное", "greentext": "Мемные Ñтрелочки", "notification_visibility": "Показывать уведомлениÑ", - "notification_visibility_follows": "ПодпиÑки", + "notification_visibility_follows": "Ðовые читатели", "notification_visibility_likes": "Лайки", "notification_visibility_mentions": "УпоминаниÑ", "notification_visibility_repeats": "Повторы", - "no_rich_text_description": "Убрать форматирование из вÑех поÑтов", + "no_rich_text_description": "Убрать форматирование из вÑех ÑтатуÑов", "hide_follows_description": "Ðе показывать кого Ñ Ñ‡Ð¸Ñ‚Ð°ÑŽ", "hide_followers_description": "Ðе показывать кто читает менÑ", "hide_follows_count_description": "Ðе показывать чиÑло читаемых пользователей", - "hide_followers_count_description": "Ðе показывать чиÑло моих подпиÑчиков", + "hide_followers_count_description": "Ðе показывать чиÑло моих читателей", "show_admin_badge": "Показывать значок админиÑтратора в моем профиле", "show_moderator_badge": "Показывать значок модератора в моем профиле", - "nsfw_clickthrough": "Включить Ñкрытие вложений и предпроÑмотра ÑÑылок Ð´Ð»Ñ NSFW ÑтатуÑов", + "nsfw_clickthrough": "Включить Ñкрытие вложений и предпроÑмотра ÑÑылок Ð´Ð»Ñ ÑтатуÑов щекотливого характера", "oauth_tokens": "OAuth токены", "token": "Токен", "refresh_token": "Рефреш токен", @@ -253,14 +262,14 @@ "radii_help": "Скругление углов Ñлементов интерфейÑа (в пикÑелÑÑ…)", "replies_in_timeline": "Ответы в ленте", "reply_visibility_all": "Показывать вÑе ответы", - "reply_visibility_following": "Показывать только ответы мне или тех на кого Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñан", + "reply_visibility_following": "Показывать только ответы мне или тем кого Ñ Ñ‡Ð¸Ñ‚Ð°ÑŽ", "reply_visibility_self": "Показывать только ответы мне", - "autohide_floating_post_button": "ÐвтоматичеÑки Ñкрывать кнопку поÑтинга (в мобильной верÑии)", + "autohide_floating_post_button": "ÐвтоматичеÑки Ñкрывать кнопку \"ÐапиÑать новый ÑтатуÑ\" (в мобильной верÑии)", "saving_err": "Ðе удалоÑÑŒ Ñохранить наÑтройки", "saving_ok": "Сохранено", "security_tab": "БезопаÑноÑÑ‚ÑŒ", - "scope_copy": "Копировать видимоÑÑ‚ÑŒ поÑта при ответе (вÑегда включено Ð´Ð»Ñ Ð›Ð¸Ñ‡Ð½Ñ‹Ñ… Сообщений)", - "minimal_scopes_mode": "Минимизировать набор опций видимоÑти поÑта", + "scope_copy": "Копировать видимоÑÑ‚ÑŒ поÑта при ответе (вÑегда включено Ð´Ð»Ñ Ð»Ð¸Ñ‡Ð½Ñ‹Ñ… Ñообщений)", + "minimal_scopes_mode": "Показывать только личное Ñообщение и публичный ÑÑ‚Ð°Ñ‚ÑƒÑ Ð² опциÑÑ… видимоÑти", "set_new_avatar": "Загрузить новый аватар", "set_new_profile_background": "Загрузить новый фон профилÑ", "set_new_profile_banner": "Загрузить новый баннер профилÑ", @@ -269,7 +278,7 @@ "stop_gifs": "Проигрывать GIF анимации только при наведении", "streaming": "Включить автоматичеÑкую загрузку новых Ñообщений при прокрутке вверх", "useStreamingApi": "Получать ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¸ ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð² реальном времени", - "useStreamingApiWarning": "(Ðе рекомендуетÑÑ, ÑкÑпериментально, ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¼Ð¾Ð³ÑƒÑ‚ пропадать)", + "useStreamingApiWarning": "(Ðе рекомендуетÑÑ, ÑкÑпериментально, ÑтатуÑÑ‹ могут пропадать)", "text": "ТекÑÑ‚", "theme": "Тема", "theme_help": "ИÑпользуйте шеÑтнадцатеричные коды цветов (#rrggbb) Ð´Ð»Ñ Ð½Ð°Ñтройки темы.", @@ -301,7 +310,8 @@ "older_version_imported": "Файл, который вы импортировали, был Ñделан в Ñтарой верÑии фронт-Ñнда.", "future_version_imported": "Файл, который вы импортировали, был Ñделан в новой верÑии фронт-Ñнда.", "v2_imported": "Файл, который вы импортировали, был Ñделан под Ñтарый фронт-Ñнд. Мы ÑтараемÑÑ ÑƒÐ»ÑƒÑ‡ÑˆÐ¸Ñ‚ÑŒ ÑовмеÑтимоÑÑ‚ÑŒ, но вÑе еще возможны неÑоÑтыковки.", - "upgraded_from_v2": "Фронт-Ñнд Pleroma был изменен. Ð’Ñ‹Ð±Ñ€Ð°Ð½Ð½Ð°Ñ Ñ‚ÐµÐ¼Ð° может выглÑдеть Ñлегка по-другому." + "upgraded_from_v2": "Фронт-Ñнд Pleroma был изменен. Ð’Ñ‹Ð±Ñ€Ð°Ð½Ð½Ð°Ñ Ñ‚ÐµÐ¼Ð° может выглÑдеть Ñлегка по-другому.", + "fe_downgraded": "ВерÑÐ¸Ñ Ñ„Ñ€Ð¾Ð½Ñ‚-Ñнда Pleroma была откачена." } }, "common": { @@ -333,13 +343,29 @@ "badge": "Фон значков", "badge_notification": "УведомлениÑ", "panel_header": "Заголовок панели", - "top_bar": "ВернÑÑ Ð¿Ð¾Ð»Ð¾Ñка", + "top_bar": "ВерхнÑÑ Ð¿Ð¾Ð»Ð¾Ñка", "borders": "Границы", "buttons": "Кнопки", "inputs": "ÐŸÐ¾Ð»Ñ Ð²Ð²Ð¾Ð´Ð°", "faint_text": "Маловажный текÑÑ‚", - "post": "Ð¡Ð¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð¸ опиÑание пользователÑ", - "alert_neutral": "Ðейтральный" + "post": "СтатуÑÑ‹ и раздел \"О Ñебе\"", + "alert_neutral": "Ðейтральный", + "alert_warning": "Предупреждение", + "selectedPost": "Выбранный ÑтатуÑ", + "pressed": "Ðажатие", + "highlight": "Выделенные Ñлементы", + "icons": "Иконки", + "poll": "График результатов опроÑа", + "wallpaper": "Фон", + "chat": { + "border": "Границы", + "outgoing": "ИÑходÑщие", + "incoming": "ВходÑщие" + }, + "tabs": "Вкладки", + "toggled": "Включено", + "disabled": "Отключено", + "selectedMenu": "Выбранный пункт меню" }, "radii": { "_tab_label": "Скругление" @@ -364,8 +390,8 @@ "panel": "Панель", "panelHeader": "Заголовок панели", "topBar": "ВерхнÑÑ Ð¿Ð¾Ð»Ð¾Ñка", - "avatar": "Ðватарка (профиль)", - "avatarStatus": "Ðватарка (в ленте)", + "avatar": "Ðватар (профиль)", + "avatarStatus": "Ðватар (в ленте)", "popup": "Ð’Ñплывающие подÑказки", "button": "Кнопки", "buttonHover": "Кнопки (наведен курÑор)", @@ -381,7 +407,7 @@ "interface": "ИнтерфейÑ", "input": "ÐŸÐ¾Ð»Ñ Ð²Ð²Ð¾Ð´Ð°", "post": "ТекÑÑ‚ поÑтов", - "postCode": "Моноширинный текÑÑ‚ в поÑте (форматирование)" + "postCode": "Моноширинный текÑÑ‚ в ÑтатуÑе (форматирование)" }, "family": "Шрифт", "size": "Размер (в пикÑелÑÑ…)", @@ -403,12 +429,12 @@ "link": "ÑÑылка" } }, - "allow_following_move": "Разрешить автоматичеÑки читать новый аккаунт при перемещении на другой Ñервер", + "allow_following_move": "ÐвтоматичеÑки начать читать новый профиль при переезде", "hide_user_stats": "Ðе показывать ÑтатиÑтику пользователей (например количеÑтво читателей)", - "discoverable": "Разрешить показ аккаунта в поиÑковиках и других ÑервиÑах", - "default_vis": "ВидимоÑÑ‚ÑŒ поÑтов по умолчанию", + "discoverable": "Разрешить показывать учетную запиÑÑŒ в поиÑковых ÑиÑтемах и прочих ÑервиÑах", + "default_vis": "ВидимоÑÑ‚ÑŒ ÑтатуÑов по умолчанию", "mutes_and_blocks": "Блокировки и игнорируемые", - "composing": "СоÑтавление поÑтов", + "composing": "СоÑтавление ÑтатуÑов", "chatMessageRadius": "Ð¡Ð¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ð² беÑеде", "blocks_tab": "Блокировки", "import_mutes_from_a_csv_file": "Импортировать игнорируемых из CSV файла", @@ -428,12 +454,12 @@ "post_status_content_type": "Формат ÑоÑтавлÑемых ÑтатуÑов по умолчанию", "subject_line_noop": "Ðе копировать", "subject_line_mastodon": "Как в Mastodon: Ñкопировать как еÑÑ‚ÑŒ", - "subject_line_email": "Как в e-mail: \"re: тема\"", + "subject_line_email": "Как в Ñлектронной почте: \"re: тема\"", "subject_line_behavior": "Копировать тему в ответах", "no_mutes": "Ðет игнорируемых", "no_blocks": "Ðет блокировок", "notification_visibility_emoji_reactions": "Реакции", - "notification_visibility_moves": "Миграции пользователей", + "notification_visibility_moves": "Переезды", "use_contain_fit": "Ðе обрезать Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð² миниатюрах", "profile_fields": { "value": "Значение", @@ -448,7 +474,7 @@ "hide_filtered_statuses": "Ðе показывать отфильтрованные ÑтатуÑÑ‹", "hide_muted_posts": "Ðе показывать ÑтатуÑÑ‹ игнорируемых пользователей", "hide_post_stats": "Ðе показывать ÑтатиÑтику ÑтатуÑов (например количеÑтво отметок «ÐравитÑÑ»)", - "use_one_click_nsfw": "Открывать NSFW Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¾Ð´Ð½Ð¸Ð¼ кликом", + "use_one_click_nsfw": "Открывать Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¸Ð¼ÐµÑŽÑ‰Ð¸Ðµ щекотливый характер одним кликом", "preload_images": "Предварительно загружать изображениÑ", "max_thumbnails": "МакÑимальное чиÑло миниатюр показываемых в ÑтатуÑе", "emoji_reactions_on_timeline": "Показывать Ñмодзи реакции в ленте", @@ -460,26 +486,43 @@ "virtual_scrolling": "Оптимизировать рендеринг ленты", "hide_wallpaper": "Скрыть обои узла", "accent": "Ðкцент", - "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 пикÑелей.", + "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-уведомлений" + "notification_setting_hide_notification_contents": "Скрыть Ð¾Ñ‚Ð¿Ñ€Ð°Ð²Ð¸Ñ‚ÐµÐ»Ñ Ð¸ Ñодержимое push-уведомлений", + "version": { + "title": "ВерÑиÑ", + "frontend_version": "ВерÑÐ¸Ñ Ñ„Ñ€Ð¾Ð½Ñ‚-Ñнда", + "backend_version": "ВерÑÐ¸Ñ Ð±Ñк-Ñнда" + }, + "word_filter": "Фильтр Ñлов", + "sensitive_by_default": "Помечать ÑтатуÑÑ‹ как имеющие щекотливый характер по умолчанию", + "reply_visibility_self_short": "Показывать ответы только вам", + "reply_visibility_following_short": "Показывать ответы тем кого вы читаете", + "hide_all_muted_posts": "Ðе показывать игнорируемые ÑтатуÑÑ‹", + "hide_media_previews": "Ðе показывать Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð² ленте", + "setting_changed": "ОтличаетÑÑ Ð¾Ñ‚ Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¿Ð¾ умолчанию", + "reset_background_confirm": "Ð’Ñ‹ точно хотите ÑброÑить фон?", + "reset_banner_confirm": "Ð’Ñ‹ точно хотите ÑброÑить баннер?", + "type_domains_to_mute": "ПоиÑк узлов, которые вы хотите заглушить", + "more_settings": "ОÑтальные наÑтройки", + "save": "Сохранить изменениÑ" }, "timeline": { "collapse": "Свернуть", "conversation": "Разговор", "error_fetching": "Ошибка при обновлении", "load_older": "Загрузить Ñтарые ÑтатуÑÑ‹", - "no_retweet_hint": "ПоÑÑ‚ помечен как \"только Ð´Ð»Ñ Ð¿Ð¾Ð´Ð¿Ð¸Ñчиков\" или \"личное\" и поÑтому не может быть повторён", + "no_retweet_hint": "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ Ð¿Ð¾Ð¼ÐµÑ‡ÐµÐ½ как \"только Ð´Ð»Ñ Ñ‡Ð¸Ñ‚Ð°Ñ‚ÐµÐ»ÐµÐ¹\" или \"личное Ñообщение\" и потому не может быть повторён", "repeated": "повторил(а)", "show_new": "Показать новые", "up_to_date": "Обновлено", @@ -488,7 +531,7 @@ "status": { "bookmark": "Добавить в закладки", "unbookmark": "Удалить из закладок", - "status_deleted": "ПоÑÑ‚ удален", + "status_deleted": "Ð¡Ñ‚Ð°Ñ‚ÑƒÑ ÑƒÐ´Ð°Ð»ÐµÐ½", "reply_to": "Ответ", "repeats": "Повторы", "favorites": "ПонравилоÑÑŒ", @@ -507,7 +550,6 @@ "follow": "Читать", "follow_sent": "Ð—Ð°Ð¿Ñ€Ð¾Ñ Ð¾Ñ‚Ð¿Ñ€Ð°Ð²Ð»ÐµÐ½!", "follow_progress": "Запрашиваем…", - "follow_again": "ЗапроÑить еще раз?", "follow_unfollow": "ПереÑтать читать", "followees": "Читаемые", "followers": "Читатели", @@ -524,16 +566,16 @@ "revoke_admin": "Забрать права админиÑтратора", "grant_moderator": "Сделать модератором", "revoke_moderator": "Забрать права модератора", - "activate_account": "Ðктивировать аккаунт", - "deactivate_account": "Деактивировать аккаунт", - "delete_account": "Удалить аккаунт", - "force_nsfw": "Отмечать поÑÑ‚Ñ‹ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ ÐºÐ°Ðº NSFW", - "strip_media": "Убирать Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¸Ð· поÑтов пользователÑ", - "force_unlisted": "Ðе добавлÑÑ‚ÑŒ поÑÑ‚Ñ‹ в публичные ленты", + "activate_account": "Ðктивировать учетную запиÑÑŒ", + "deactivate_account": "Деактивировать учетную запиÑÑŒ", + "delete_account": "Удалить учетную запиÑÑŒ", + "force_nsfw": "Отмечать ÑтатуÑÑ‹ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ ÐºÐ°Ðº имеющие щекотливый характер", + "strip_media": "Убирать Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ð¸Ð· ÑтатуÑов пользователÑ", + "force_unlisted": "Ðе показывать ÑтатуÑÑ‹ в публичных лентах", "sandbox": "Принудить видимоÑÑ‚ÑŒ поÑтов только читателÑм", - "disable_remote_subscription": "Запретить читать Ñ ÑƒÐ´Ð°Ð»ÐµÐ½Ð½Ñ‹Ñ… Ñерверов", + "disable_remote_subscription": "Запретить читать Ñ Ð´Ñ€ÑƒÐ³Ð¸Ñ… узлов", "disable_any_subscription": "Запретить читать пользователÑ", - "quarantine": "Ðе федерировать поÑÑ‚Ñ‹ пользователÑ", + "quarantine": "Ðе федерировать ÑтатуÑÑ‹ пользователÑ", "delete_user": "Удалить пользователÑ", "delete_user_confirmation": "Ð’Ñ‹ уверены? Ðто дейÑтвие Ð½ÐµÐ»ÑŒÐ·Ñ Ð¾Ñ‚Ð¼ÐµÐ½Ð¸Ñ‚ÑŒ." }, @@ -542,9 +584,12 @@ "show_repeats": "Показывать повторы", "hide_repeats": "Скрыть повторы", "report": "ПожаловатьÑÑ", - "roles": { - "moderator": "Модератор", - "admin": "ÐдминиÑтратор" + "message": "ÐапиÑать Ñообщение", + "highlight": { + "side": "ПолоÑка Ñбоку", + "striped": "Фон в полоÑку", + "solid": "Сплошной фон", + "disabled": "Ðет выделениÑ" } }, "user_profile": { @@ -560,30 +605,31 @@ "password_reset": { "forgot_password": "Забыли пароль?", "password_reset": "Ð¡Ð±Ñ€Ð¾Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ", - "instruction": "Введите ваш email или Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ, и мы отправим вам ÑÑылку Ð´Ð»Ñ ÑброÑа паролÑ.", - "placeholder": "Ваш email или Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ", - "check_email": "Проверьте ваш email и перейдите по ÑÑылке Ð´Ð»Ñ ÑброÑа паролÑ.", + "instruction": "Введите ваш Ð°Ð´Ñ€ÐµÑ Ñлектронной почты или Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ: на вашу Ñлектронную почту будет отправлена ÑÑылка Ð´Ð»Ñ ÑброÑа паролÑ.", + "placeholder": "Ваш Ð°Ð´Ñ€ÐµÑ Ñлектронной почты или Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ", + "check_email": "Проверьте вашу Ñлектронную почту и перейдите по ÑÑылке Ð´Ð»Ñ ÑброÑа паролÑ.", "return_home": "ВернутьÑÑ Ð½Ð° главную Ñтраницу", "too_many_requests": "Ð’Ñ‹ иÑчерпали допуÑтимое количеÑтво попыток, попробуйте позже.", - "password_reset_disabled": "Ð¡Ð±Ñ€Ð¾Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð¾Ñ‚ÐºÐ»ÑŽÑ‡ÐµÐ½. CвÑжитеÑÑŒ Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратором вашего Ñервера." + "password_reset_disabled": "ÐвтоматичеÑкий ÑÐ±Ñ€Ð¾Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð¾Ñ‚ÐºÐ»ÑŽÑ‡ÐµÐ½. СвÑжитеÑÑŒ Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратором данного узла Ð´Ð»Ñ ÑброÑа паролÑ.", + "password_reset_required_but_mailer_is_disabled": "Ð’Ñ‹ должны ÑброÑить Ñвой пароль, однако автоматичеÑкий ÑÐ±Ñ€Ð¾Ñ Ð¿Ð°Ñ€Ð¾Ð»Ñ Ð¾Ñ‚ÐºÐ»ÑŽÑ‡ÐµÐ½. ПожалуйÑта ÑвÑжитеÑÑŒ Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸Ñтратором данного узла." }, "about": { "mrf": { "federation": "ФедерациÑ", "simple": { - "accept_desc": "Данный Ñервер принимает ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ Ñо Ñледующих Ñерверов:", - "ftl_removal_desc": "Данный Ñервер Ñкрывает Ñледующие Ñервера Ñ Ñ„ÐµÐ´ÐµÑ€Ð°Ñ‚Ð¸Ð²Ð½Ð¾Ð¹ ленты:", - "media_nsfw_desc": "Данный Ñервер принужденно помечает Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ñо Ñледущих Ñерверов как NSFW:", - "simple_policies": "Правила Ð´Ð»Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð½Ñ‹Ñ… Ñерверов", - "accept": "Принимаемые ÑообщениÑ", - "reject": "ОтклонÑемые ÑообщениÑ", - "reject_desc": "Данный Ñервер не принимает ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ñо Ñледующих Ñерверов:", + "accept_desc": "Данный узел принимает ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ñ‚Ð¾Ð»ÑŒÐºÐ¾ Ñо Ñледующих узлов:", + "ftl_removal_desc": "Данный узел Ñкрывает Ñледующие узлы Ñ Ñ„ÐµÐ´ÐµÑ€Ð°Ñ‚Ð¸Ð²Ð½Ð¾Ð¹ ленты:", + "media_nsfw_desc": "Данный узел принужденно помечает Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ñо Ñледующих узлов как имеющие щекотливый характер:", + "simple_policies": "Правила Ð´Ð»Ñ Ð¾Ð¿Ñ€ÐµÐ´ÐµÐ»ÐµÐ½Ð½Ñ‹Ñ… узлов", + "accept": "Белый ÑпиÑок", + "reject": "Черный ÑпиÑок", + "reject_desc": "Данный узел не принимает ÑÐ¾Ð¾Ð±Ñ‰ÐµÐ½Ð¸Ñ Ñо Ñледующих узлов:", "quarantine": "Зона карантина", - "quarantine_desc": "Данный Ñервер отправлÑет только публичные поÑÑ‚Ñ‹ Ñледующим Ñерверам:", + "quarantine_desc": "Данный узел отправлÑет только публичные ÑтатуÑÑ‹ Ñледующим узлам:", "ftl_removal": "Скрытие Ñ Ñ„ÐµÐ´ÐµÑ€Ð°Ñ‚Ð¸Ð²Ð½Ð¾Ð¹ ленты", "media_removal": "Удаление вложений", - "media_removal_desc": "Данный Ñервер удалÑет Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ñо Ñледующих Ñерверов:", - "media_nsfw": "Принужденно помеченно как NSFW" + "media_removal_desc": "Данный узел удалÑет Ð²Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ Ñо Ñледующих узлов:", + "media_nsfw": "Принужденно помеченно как имеющее щекотливый характер" }, "keyword": { "ftl_removal": "Убрать из федеративной ленты", @@ -593,7 +639,7 @@ "is_replaced_by": "→" }, "mrf_policies": "Ðктивные правила MRF (модуль перепиÑÑ‹Ð²Ð°Ð½Ð¸Ñ Ñообщений)", - "mrf_policies_desc": "Правила MRF (модуль перепиÑÑ‹Ð²Ð°Ð½Ð¸Ñ Ñообщений) влиÑÑŽÑ‚ на федерацию данного Ñервера. Следующие правила активны:" + "mrf_policies_desc": "Правила MRF (модуль перепиÑÑ‹Ð²Ð°Ð½Ð¸Ñ Ñообщений) влиÑÑŽÑ‚ на федерацию данного узла. Следующие правила активны:" }, "staff": "ÐдминиÑтрациÑ" }, @@ -615,7 +661,8 @@ "gopher": "Gopher", "who_to_follow": "ÐŸÑ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ ÐºÐ¾Ð³Ð¾ читать", "pleroma_chat_messages": "Pleroma Чат", - "upload_limit": "Ðаибольший размер загружаемого файла" + "upload_limit": "Ðаибольший размер загружаемого файла", + "scope_options": "ÐаÑÑ‚Ñ€Ð°Ð¸Ð²Ð°ÐµÐ¼Ð°Ñ Ð²Ð¸Ð´Ð¸Ð¼Ð¾ÑÑ‚ÑŒ ÑтатуÑов" }, "tool_tip": { "accept_follow_request": "ПринÑÑ‚ÑŒ Ð·Ð°Ð¿Ñ€Ð¾Ñ Ð½Ð° чтение", @@ -643,7 +690,9 @@ "votes": "голоÑов", "option": "Вариант", "add_option": "Добавить вариант", - "add_poll": "Прикрепить опроÑ" + "add_poll": "Прикрепить опроÑ", + "votes_count": "{count} Ð³Ð¾Ð»Ð¾Ñ | {count} голоÑов", + "people_voted_count": "{count} человек проголоÑовал | {count} человек проголоÑовали" }, "media_modal": { "next": "СледующаÑ", @@ -701,10 +750,26 @@ "chats": "БеÑеды", "delete": "Удалить", "message_user": "Ðапишите {nickname}", - "you": "Ð’Ñ‹:" + "you": "Ð’Ñ‹:", + "error_sending_message": "Произошла ошибка при отправке ÑообщениÑ." }, "remote_user_resolver": { "error": "Ðе найдено.", "searching_for": "Ищем" + }, + "upload": { + "error": { + "message": "Произошла ошибка при загрузке: {0}" + } + }, + "user_reporting": { + "add_comment_description": "Жалоба будет направлена модераторам вашего узла. Ð’Ñ‹ можете указать причину жалобы ниже:", + "forward_description": "Данный пользователь находитÑÑ Ð½Ð° другом узле. ОтоÑлать туда копию вашей жалобы?" + }, + "file_type": { + "file": "Файл", + "video": "ВидеозапиÑÑŒ", + "audio": "ÐудиозапиÑÑŒ", + "image": "Изображение" } } diff --git a/src/i18n/te.json b/src/i18n/te.json index bb68d29e47b8e6e825051713f029996b51de3a8b..1216de5955e5d4752cc4b5a5dc5a37c0490e691b 100644 --- a/src/i18n/te.json +++ b/src/i18n/te.json @@ -310,7 +310,6 @@ "user_card.follow": "Follow", "user_card.follow_sent": "Request sent!", "user_card.follow_progress": "Requesting…", - "user_card.follow_again": "Send request again?", "user_card.follow_unfollow": "Unfollow", "user_card.followees": "Following", "user_card.followers": "Followers", diff --git a/src/i18n/uk.json b/src/i18n/uk.json index 040d6f4f76b194ef98ec9793b952e1e53a539099..d98330874b8908dfdeacac7c7c1d09b6ac8a54c3 100644 --- a/src/i18n/uk.json +++ b/src/i18n/uk.json @@ -17,7 +17,14 @@ "more": "Більше", "submit": "Відправити", "apply": "ЗаÑтоÑувати", - "peek": "ГлÑнути" + "peek": "ГлÑнути", + "role": { + "moderator": "Модератор", + "admin": "ÐдмініÑтратор" + }, + "flash_content": "ÐатиÑніть Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÐ³Ð»Ñду зміÑту Flash за допомогою Ruffle (екÑпериментально, може не працювати).", + "flash_security": "Ð¦Ñ Ñ„ÑƒÐ½ÐºÑ†Ñ–Ñ Ð¼Ð¾Ð¶Ðµ Ñтановити ризик, оÑкільки Flash-вміÑÑ‚ вÑе ще Ñ” потенційно небезпечним.", + "flash_fail": "Ðе вдалоÑÑ Ð·Ð°Ð²Ð°Ð½Ñ‚Ð°Ð¶Ð¸Ñ‚Ð¸ Flash-вміÑÑ‚, докладнішу інформацію дивиÑÑŒ у конÑолі." }, "finder": { "error_fetching_user": "КориÑтувача не знайдено", @@ -26,7 +33,7 @@ "features_panel": { "gopher": "Gopher", "pleroma_chat_messages": "Чати", - "chat": "Міні-чат", + "chat": "ОголошеннÑ", "who_to_follow": "Кого відÑтежувати", "title": "ОÑобливоÑÑ‚Ñ–", "scope_options": "Параметри обÑÑгу", @@ -45,7 +52,7 @@ "mute": "Ігнорувати" }, "shoutbox": { - "title": "Міні-чат" + "title": "ОголошеннÑ" }, "about": { "staff": "ÐдмініÑтраціÑ", @@ -118,7 +125,9 @@ "votes": "голоÑів", "option": "Відповідь", "add_poll": "Додати опитуваннÑ", - "not_enough_options": "Замало унікальних варіантів в опитуванні" + "not_enough_options": "Замало унікальних варіантів в опитуванні", + "people_voted_count": "{count} оÑоба проголоÑувала | {count} оÑіб проголоÑувало", + "votes_count": "{count} Ð³Ð¾Ð»Ð¾Ñ | {count} голоÑів" }, "notifications": { "reacted_with": "додав реакцію: {0}", @@ -151,7 +160,8 @@ "interactions": "Взаємодії", "mentions": "ЗгадуваннÑ", "back": "Ðазад", - "administration": "ÐдмініÑтруваннÑ" + "administration": "ÐдмініÑтруваннÑ", + "home_timeline": "Ð”Ð¾Ð¼Ð°ÑˆÐ½Ñ Ñтрічка" }, "media_modal": { "next": "ÐаÑтупна", @@ -242,7 +252,8 @@ }, "preview_empty": "ПуÑтий", "media_description_error": "Ðе вдалоÑÑŒ оновити медіа, Ñпробуйте ще раз", - "media_description": "ÐžÐ¿Ð¸Ñ Ð¼ÐµÐ´Ñ–Ð°" + "media_description": "ÐžÐ¿Ð¸Ñ Ð¼ÐµÐ´Ñ–Ð°", + "post": "Опублікувати" }, "settings": { "blocks_imported": "Ð‘Ð»Ð¾ÐºÑƒÐ²Ð°Ð½Ð½Ñ Ñ–Ð¼Ð¿Ð¾Ñ€Ñ‚Ð¾Ð²Ð°Ð½Ñ–! Їх обробка триватиме певний чаÑ.", @@ -604,7 +615,30 @@ "backend_version": "ВерÑÑ–Ñ Ð±ÐµÐºÐµÐ½Ð´Ñƒ", "title": "ВерÑÑ–Ñ" }, - "hide_wallpaper": "Сховати шпалери екземплÑру" + "hide_wallpaper": "Сховати шпалери екземплÑру", + "more_settings": "Більше налаштувань", + "sensitive_by_default": "Визначати Ð´Ð¾Ð¿Ð¸Ñ Ñк дратівливий за замовчуваннÑм", + "reply_visibility_self_short": "Показувати відповіді лише мені", + "reply_visibility_following_short": "Показувати відповіді тим, на кого Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñаний", + "hide_all_muted_posts": "Приховати приглушені повідомленнÑ", + "hide_media_previews": "Приховати попередній переглÑд медіа", + "word_filter": "Фільтр Ñлів", + "setting_changed": "ÐšÐ¾Ð½Ñ„Ñ–Ð³ÑƒÑ€Ð°Ñ†Ñ–Ñ Ð²Ñ–Ð´Ñ€Ñ–Ð·Ð½ÑєтьÑÑ Ð²Ñ–Ð´ типової", + "save": "Зберегти зміни", + "file_export_import": { + "errors": { + "file_slightly_new": "ДругорÑдна верÑÑ–Ñ Ñ„Ð°Ð¹Ð»Ñƒ відрізнÑєтьÑÑ, деÑкі Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¼Ð¾Ð¶ÑƒÑ‚ÑŒ бути не прийнÑÑ‚Ñ–", + "file_too_old": "ÐеÑуміÑна оÑновна верÑÑ–Ñ: {fileMajor}, верÑÑ–Ñ Ñ„Ð°Ð¹Ð»Ñƒ занадто Ñтара Ñ– не підтримуєтьÑÑ (мінімальна верÑÑ–Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½ÑŒ {feMajor})", + "file_too_new": "ÐеÑуміÑна оÑновна верÑÑ–Ñ: {fileMajor}, Ñ†Ñ Ð²ÐµÑ€ÑÑ–Ñ PleromaFE ({feMajor}) занадто Ñтара Ð´Ð»Ñ Ð¹Ð¾Ð³Ð¾ обробки", + "invalid_file": "Вибраний файл не Ñ” резервною копією налаштувань Pleroma. ÐÑ–Ñких змін не було зроблено." + }, + "restore_settings": "Відновити Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð· файлу", + "backup_settings_theme": "Резервне ÐºÐ¾Ð¿Ñ–ÑŽÐ²Ð°Ð½Ð½Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½ÑŒ та теми у файл", + "backup_settings": "Резервне ÐºÐ¾Ð¿Ñ–ÑŽÐ²Ð°Ð½Ð½Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½ÑŒ у файл", + "backup_restore": "Резервне ÐºÐ¾Ð¿Ñ–ÑŽÐ²Ð°Ð½Ð½Ñ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½ÑŒ" + }, + "right_sidebar": "Показувати бокову панель Ñправа", + "hide_shoutbox": "Приховати Ð¾Ð³Ð¾Ð»Ð¾ÑˆÐµÐ½Ð½Ñ Ñ–Ð½ÑтанÑу" }, "selectable_list": { "select_all": "Вибрати вÑе" @@ -633,7 +667,10 @@ "fullname": "Відображене ім'Ñ", "email": "Ел. пошта", "bio": "Про Ñебе", - "captcha": "CAPTCHA" + "captcha": "CAPTCHA", + "register": "ЗареєÑтруватиÑÑ", + "reason_placeholder": "Цей інÑÑ‚Ð°Ð½Ñ Ð¾Ð±Ñ€Ð¾Ð±Ð»ÑÑ” запити на реєÑтрацію вручну.\nРозкажіть адмініÑтрації чому ви хочете зареєÑтруватиÑÑ.", + "reason": "Причина реєÑтрації" }, "who_to_follow": { "who_to_follow": "Ðа кого підпиÑатиÑÑ", @@ -711,7 +748,6 @@ "message": "ПовідомленнÑ", "follow": "ПідпиÑатиÑÑŒ", "follow_unfollow": "ВідпиÑатиÑÑŒ", - "follow_again": "Відправити запит знову?", "follow_sent": "Запит відправлено!", "blocked": "Заблоковано!", "admin_menu": { @@ -761,10 +797,14 @@ "remote_follow": "ПідпиÑатиÑÑŒ", "muted": "Заглушений", "mute": "Заглушити", - "roles": { - "moderator": "Модератор", - "admin": "ÐдмініÑтратор" - } + "highlight": { + "side": "Смужка ліворуч", + "striped": "СмугаÑтий фон", + "solid": "Суцільний фон", + "disabled": "Ðе виділÑти" + }, + "bot": "Бот", + "edit_profile": "Редагувати профіль" }, "status": { "copy_link": "Скопіювати поÑÐ¸Ð»Ð°Ð½Ð½Ñ Ð½Ð° допиÑ", @@ -804,7 +844,9 @@ "conversation": "Розмова", "no_statuses": "ÐÑ–Ñких ÑтатуÑів", "repeated": "поширив(-ла)", - "no_retweet_hint": "ЗапиÑ, позначено Ñк \"тільки Ð´Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñників\" або \"оÑобиÑте\" Ñ– тому не може бути поширений" + "no_retweet_hint": "ЗапиÑ, позначено Ñк \"тільки Ð´Ð»Ñ Ð¿Ñ–Ð´Ð¿Ð¸Ñників\" або \"оÑобиÑте\" Ñ– тому не може бути поширений", + "socket_broke": "Втрачено з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ñƒ реальному чаÑÑ–: код {0}", + "socket_reconnected": "Ð’Ñтановлено з'Ñ”Ð´Ð½Ð°Ð½Ð½Ñ Ñƒ реальному чаÑÑ–" }, "user_reporting": { "submit": "Відправити", diff --git a/src/i18n/vi.json b/src/i18n/vi.json new file mode 100644 index 0000000000000000000000000000000000000000..c77ad4caff4d50e644e87a8c3259be52b9fe5dc7 --- /dev/null +++ b/src/i18n/vi.json @@ -0,0 +1,872 @@ +{ + "about": { + "mrf": { + "federation": "Liên hợp", + "keyword": { + "keyword_policies": "ChÃnh sách quan trá»ng", + "reject": "Từ chối", + "replace": "Thay thế", + "is_replaced_by": "→", + "ftl_removal": "Giá»›i hạn chung" + }, + "mrf_policies": "KÃch hoạt chÃnh sách MRF", + "simple": { + "simple_policies": "Quy tắc máy chủ", + "accept": "Äồng ý", + "accept_desc": "Máy chủ nà y chỉ chấp nháºn tin nhắn từ những máy chủ:", + "reject": "Từ chối", + "quarantine": "Bảo hà nh", + "quarantine_desc": "Máy chủ nà y sẽ gá»i tút công khai đến những máy chủ:", + "ftl_removal": "Giá»›i hạn chung", + "media_removal": "Ẩn Media", + "media_removal_desc": "Media từ những máy chủ sau sẽ bị ẩn:", + "media_nsfw": "Ãp đặt nhạy cảm", + "media_nsfw_desc": "Ná»™i dung từ những máy chủ sau sẽ bị tá»± Ä‘á»™ng gắn nhãn nhạy cảm:", + "reject_desc": "Máy chủ nà y không chấp nháºn tin nhắn từ những máy chủ:", + "ftl_removal_desc": "Ná»™i dung từ những máy chủ sau sẽ bị ẩn:" + }, + "mrf_policies_desc": "Các chÃnh sách MRF kiểm soát sá»± liên hợp của máy chủ. Các chÃnh sách sau được báºt:" + }, + "staff": "Nhân viên" + }, + "domain_mute_card": { + "mute": "Ẩn", + "mute_progress": "Äang ẩn…", + "unmute": "NgÆ°ng ẩn", + "unmute_progress": "Äang ngÆ°ng ẩn…" + }, + "exporter": { + "export": "Xuất dữ liệu", + "processing": "Äang chuẩn bị táºp tin cho bạn tải vá»" + }, + "features_panel": { + "chat": "Chat", + "pleroma_chat_messages": "Pleroma Chat", + "gopher": "Gopher", + "media_proxy": "Proxy media", + "text_limit": "Giá»›i hạn ký tá»±", + "title": "TÃnh năng", + "who_to_follow": "Äá» xuất theo dõi", + "upload_limit": "Giá»›i hạn tải lên", + "scope_options": "Äa dạng kiểu đăng" + }, + "finder": { + "error_fetching_user": "Lá»—i khi nạp ngÆ°á»i dùng", + "find_user": "Tìm ngÆ°á»i dùng" + }, + "shoutbox": { + "title": "Chat cùng nhau" + }, + "general": { + "apply": "Ãp dụng", + "submit": "Gá»i tặng", + "more": "Nhiá»u hÆ¡n", + "loading": "Äang tải…", + "generic_error": "Äã có lá»—i xảy ra", + "error_retry": "Xin hãy thá» lại", + "retry": "Thá» lại", + "optional": "tùy chá»n", + "show_more": "Xem thêm", + "show_less": "Thu gá»n", + "dismiss": "Bá» qua", + "cancel": "Hủy bá»", + "disable": "Tắt", + "enable": "Báºt", + "confirm": "Xác nháºn", + "verify": "Xác thá»±c", + "close": "Äóng", + "peek": "Thu gá»n", + "role": { + "admin": "Quản trị viên", + "moderator": "Kiểm duyệt viên" + }, + "flash_security": "LÆ°u ý rằng Ä‘iá»u nà y có thể tiá»m ẩn nguy hiểm vì ná»™i dung Flash là mã láºp trình tùy ý.", + "flash_fail": "Tải ná»™i dung Flash thất bại, tham khảo chi tiết trong console.", + "flash_content": "Nhấn để hiện ná»™i dung Flash bằng Ruffle (Thá» nghiệm, có thể không dùng được)." + }, + "image_cropper": { + "crop_picture": "Cắt hình ảnh", + "save": "LÆ°u", + "save_without_cropping": "Bá» qua cắt", + "cancel": "Hủy bá»" + }, + "importer": { + "submit": "Gá»i Ä‘i", + "success": "Äã nháºp dữ liệu thà nh công.", + "error": "Có lá»—i xảy ra khi nháºp dữ liệu từ táºp tin nà y." + }, + "login": { + "login": "Äăng nháºp", + "description": "Äăng nháºp bằng OAuth", + "logout": "Äăng xuất", + "password": "Máºt khẩu", + "placeholder": "vd: cobetronxinh", + "register": "Äăng ký", + "username": "Tên ngÆ°á»i dùng", + "hint": "Äăng nháºp để cùng trò chuyện", + "authentication_code": "Mã truy cáºp", + "enter_recovery_code": "Nháºp mã khôi phục", + "recovery_code": "Mã khôi phục", + "heading": { + "totp": "Xác thá»±c hai bÆ°á»›c", + "recovery": "Khôi phục hai bÆ°á»›c" + }, + "enter_two_factor_code": "Nháºp mã xác thá»±c hai bÆ°á»›c" + }, + "media_modal": { + "previous": "TrÆ°á»›c đó", + "next": "Kế tiếp" + }, + "nav": { + "about": "Vá» máy chủ nà y", + "administration": "Váºn hà nh bởi", + "back": "Quay lại", + "friend_requests": "Yêu cầu theo dõi", + "mentions": "Lượt nhắc đến", + "interactions": "Giao tiếp", + "dms": "Nhắn tin", + "public_tl": "Bảng tin máy chủ", + "timeline": "Bảng tin", + "home_timeline": "Bảng tin của bạn", + "twkn": "Thế giá»›i", + "bookmarks": "Äã lÆ°u", + "user_search": "Tìm kiếm ngÆ°á»i dùng", + "search": "Tìm kiếm", + "who_to_follow": "Äá» xuất theo dõi", + "preferences": "Thiết láºp", + "timelines": "Bảng tin", + "chats": "Chat" + }, + "notifications": { + "broken_favorite": "Trạng thái chÆ°a rõ, Ä‘ang tìm kiếm…", + "favorited_you": "thÃch tút của bạn", + "followed_you": "theo dõi bạn", + "follow_request": "yêu cầu theo dõi bạn", + "load_older": "Xem những thông báo cÅ© hÆ¡n", + "notifications": "Thông báo", + "read": "Äá»c!", + "repeated_you": "chia sẻ tút của bạn", + "no_more_notifications": "Không còn thông báo nà o", + "migrated_to": "chuyển sang", + "reacted_with": "chạm tá»›i {0}", + "error": "Lá»—i khi nạp thông báo {0}" + }, + "polls": { + "add_poll": "Tạo bình chá»n", + "option": "Lá»±a chá»n", + "votes": "ngÆ°á»i bình chá»n", + "people_voted_count": "{count} ngÆ°á»i bình chá»n | {count} ngÆ°á»i bình chá»n", + "vote": "Bình chá»n", + "type": "Kiểu bình chá»n", + "single_choice": "Chỉ được chá»n má»™t lá»±a chá»n", + "multiple_choices": "Cho phép chá»n nhiá»u lá»±a chá»n", + "expiry": "Thá»i hạn bình chá»n", + "expires_in": "Bình chá»n kết thúc sau {0}", + "not_enough_options": "Không đủ lá»±a chá»n tối thiểu", + "add_option": "Thêm lá»±a chá»n", + "votes_count": "{count} bình chá»n | {count} bình chá»n", + "expired": "Bình chá»n đã kết thúc {0} trÆ°á»›c" + }, + "emoji": { + "stickers": "Sticker", + "emoji": "Emoji", + "keep_open": "Mở khung lá»±a chá»n", + "search_emoji": "Tìm emoji", + "add_emoji": "Nháºp emoji", + "custom": "Tùy chỉnh emoji", + "unicode": "Unicode emoji", + "load_all_hint": "Tải trÆ°á»›c {saneAmount} emoji, tải toà n bá»™ emoji có thể gây xá» là cháºm.", + "load_all": "Äang tải {emojiAmount} emoji" + }, + "interactions": { + "favs_repeats": "TÆ°Æ¡ng tác", + "follows": "Lượt theo dõi má»›i", + "moves": "NgÆ°á»i dùng chuyển Ä‘i", + "load_older": "Xem tÆ°Æ¡ng tác cÅ© hÆ¡n" + }, + "post_status": { + "new_status": "Äăng tút", + "account_not_locked_warning": "Tà i khoản của bạn chÆ°a {0}. Bất kỳ ai cÅ©ng có thể xem những tút dà nh cho ngÆ°á»i theo dõi của bạn.", + "account_not_locked_warning_link": "đã khóa", + "attachments_sensitive": "Äánh dấu media là nhạy cảm", + "media_description": "Mô tả media", + "content_type": { + "text/plain": "Văn bản", + "text/html": "HTML", + "text/markdown": "Markdown", + "text/bbcode": "BBCode" + }, + "content_warning": "Tiêu Ä‘á» (tùy chá»n)", + "default": "Äá»i ngÆ°á»i con gái không muốn yêu ai được không?", + "direct_warning_to_first_only": "NgÆ°á»i đầu tiên được nhắc đến má»›i có thể thấy tút nà y.", + "posting": "Äang đăng tút", + "post": "Äăng", + "preview": "Xem trÆ°á»›c", + "preview_empty": "Trống", + "empty_status_error": "Không thể đăng má»™t tút trống và không có media", + "media_description_error": "Cáºp nháºt media thất bại, thá» lại sau", + "scope_notice": { + "private": "Chỉ những ngÆ°á»i theo dõi bạn má»›i thấy tút nà y", + "unlisted": "Tút nà y sẽ không hiện trong bảng tin máy chủ và thế giá»›i", + "public": "Má»i ngÆ°á»i Ä‘á»u có thể thấy tút nà y" + }, + "scope": { + "public": "Công khai - hiện trên bảng tin máy chủ", + "private": "Riêng tÆ° - Chỉ dà nh cho ngÆ°á»i theo dõi", + "unlisted": "Hạn chế - không hiện trên bảng tin", + "direct": "Tin nhắn - chỉ ngÆ°á»i được nhắc đến má»›i thấy" + }, + "direct_warning_to_all": "Những ai được nhắc đến sẽ Ä‘á»u thấy tút nà y." + }, + "registration": { + "bio": "Tiểu sá»", + "email": "Email", + "fullname": "Tên hiển thị", + "password_confirm": "Xác nháºn máºt khẩu", + "registration": "Äăng ký", + "token": "Lá»i má»i", + "captcha": "CAPTCHA", + "new_captcha": "Nhấn và o hình ảnh để đổi captcha má»›i", + "username_placeholder": "vd: cobetronxinh", + "fullname_placeholder": "vd: Cô Bé Tròn Xinh", + "bio_placeholder": "vd:\nHi, I'm Cô Bé Tròn Xinh.\nI’m an anime girl living in suburban Vietnam. You may know me from the school.", + "reason": "Lý do đăng ký", + "reason_placeholder": "Máy chủ nà y phê duyệt đăng ký thủ công.\nHãy cho quản trị viên biết lý do bạn muốn đăng ký.", + "register": "Äăng ký", + "validations": { + "username_required": "không được để trống", + "fullname_required": "không được để trống", + "email_required": "không được để trống", + "password_confirmation_required": "không được để trống", + "password_confirmation_match": "phải trùng khá»›p vá»›i máºt khẩu", + "password_required": "không được để trống" + } + }, + "remote_user_resolver": { + "remote_user_resolver": "Giải quyết ngÆ°á»i dùng từ xa", + "searching_for": "Tìm kiếm", + "error": "Không tìm thấy." + }, + "selectable_list": { + "select_all": "Chá»n tất cả" + }, + "settings": { + "app_name": "Tên app", + "save": "LÆ°u thay đổi", + "security": "Bảo máºt", + "enter_current_password_to_confirm": "Nháºp máºt khẩu để xác thá»±c", + "mfa": { + "otp": "OTP", + "setup_otp": "Thiết láºp OTP", + "wait_pre_setup_otp": "háºu thiết láºp OTP", + "confirm_and_enable": "Xác nháºn và kÃch hoạt OTP", + "title": "Xác thá»±c hai bÆ°á»›c", + "recovery_codes": "Những mã khôi phục.", + "waiting_a_recovery_codes": "Äang nháºn mã khôi phục…", + "authentication_methods": "PhÆ°Æ¡ng pháp xác thá»±c", + "scan": { + "title": "Quét", + "desc": "Sá» dụng app xác thá»±c hai bÆ°á»›c để quét mã QR hoặc nháºp mã khôi phục:", + "secret_code": "Mã" + }, + "verify": { + "desc": "Äể báºt xác thá»±c hai bÆ°á»›c, nháºp mã từ app của bạn:" + }, + "generate_new_recovery_codes": "Tạo mã khôi phục má»›i", + "warning_of_generate_new_codes": "Khi tạo mã khôi phục má»›i, những mã khôi phục cÅ© sẽ không sá» dụng được nữa.", + "recovery_codes_warning": "Hãy viết lại mã và cất ở má»™t nÆ¡i an toà n - những mã nà y sẽ không xuất hiện lại nữa. Nếu mất quyá»n sá» dụng app 2FA app và mã khôi phục, tà i khoản của bạn sẽ không thể truy cáºp." + }, + "allow_following_move": "Cho phép tá»± Ä‘á»™ng theo dõi lại khi tà i khoản Ä‘ang theo dõi chuyển sang máy chủ khác", + "attachmentRadius": "Táºp tin tải lên", + "attachments": "Táºp tin tải lên", + "avatar": "Ảnh đại diện", + "avatarAltRadius": "Ảnh đại diện (thông báo)", + "avatarRadius": "Ảnh đại diện", + "background": "Ảnh ná»n", + "bio": "Tiểu sá»", + "block_export": "Xuất danh sách chặn", + "block_import": "Nháºp danh sách chặn", + "block_import_error": "Lá»—i khi nháºp danh sách chặn", + "mute_export": "Xuất danh sách ẩn", + "mute_export_button": "Xuất danh sách ẩn ra táºp tin CSV", + "mute_import": "Nháºp danh sách ẩn", + "mute_import_error": "Lá»—i khi nháºp danh sách ẩn", + "mutes_imported": "Äã nháºp danh sách ẩn! Sẽ mất má»™t lúc nữa để hoà n thà nh.", + "import_mutes_from_a_csv_file": "Nháºp danh sách ẩn từ táºp tin CSV", + "blocks_tab": "Danh sách chặn", + "bot": "Äây là tà i khoản Bot", + "btnRadius": "Nút", + "cBlue": "Xanh (Trả lá»i, theo dõi)", + "cOrange": "Cam (ThÃch)", + "cRed": "Äá» (Hủy bá»)", + "change_email": "Äổi email", + "change_email_error": "Có lá»—i xảy ra khi đổi email.", + "changed_email": "Äã đổi email thà nh công!", + "change_password": "Äổi máºt khẩu", + "changed_password": "Äổi máºt khẩu thà nh công!", + "chatMessageRadius": "Tin nhắn chat", + "follows_imported": "Äã nháºp danh sách theo dõi! Sẽ mất má»™t lúc nữa để hoà n thà nh.", + "collapse_subject": "Thu gá»n những tút có tá»±a Ä‘á»", + "composing": "Thu gá»n", + "current_password": "Máºt khẩu cÅ©", + "mutes_and_blocks": "Ẩn và Chặn", + "data_import_export_tab": "Nháºp / Xuất dữ liệu", + "default_vis": "Kiểu đăng tút mặc định", + "delete_account": "Xóa tà i khoản", + "delete_account_error": "Có lá»—i khi xóa tà i khoản. Xin liên hệ quản trị viên máy chủ để tìm hiểu.", + "delete_account_instructions": "Nháºp máºt khẩu bên dÆ°á»›i để xác nháºn.", + "domain_mutes": "Máy chủ", + "avatar_size_instruction": "KÃch cỡ tối thiểu 150x150 pixels.", + "pad_emoji": "Nhá»› chừa khoảng cách khi chèn emoji", + "emoji_reactions_on_timeline": "Hiện tÆ°Æ¡ng tác emoji trên bảng tin", + "export_theme": "LÆ°u mẫu", + "filtering": "Bá»™ lá»c", + "filtering_explanation": "Những tút chứa từ sau sẽ bị ẩn, má»—i chữ má»™t hà ng", + "word_filter": "Bá»™ lá»c từ ngữ", + "follow_export": "Xuất danh sách theo dõi", + "follow_import": "Nháºp danh sách theo dõi", + "follow_import_error": "Lá»—i khi nháºp danh sách theo dõi", + "accent": "Mà u chủ đạo", + "foreground": "Mà u phối", + "general": "Chung", + "hide_attachments_in_convo": "Ẩn táºp tin Ä‘Ãnh kèm trong thảo luáºn", + "hide_media_previews": "Ẩn xem trÆ°á»›c media", + "hide_all_muted_posts": "Ẩn những tút đã ẩn", + "hide_muted_posts": "Ẩn tút từ các ngÆ°á»i dùng đã ẩn", + "max_thumbnails": "Số ảnh xem trÆ°á»›c tối Ä‘a cho má»—i tút", + "hide_isp": "Ẩn thanh bên của máy chủ", + "hide_shoutbox": "Ẩn thanh chat máy chủ", + "hide_wallpaper": "Ẩn ảnh ná»n máy chủ", + "preload_images": "Tải trÆ°á»›c hình ảnh", + "use_one_click_nsfw": "Xem ná»™i dung nhạy cảm bằng cách nhấn và o", + "hide_user_stats": "Ẩn số liệu ngÆ°á»i dùng (vd: số ngÆ°á»i theo dõi)", + "hide_filtered_statuses": "Ẩn những tút đã lá»c", + "import_followers_from_a_csv_file": "Nháºp danh sách theo dõi từ táºp tin CSV", + "import_theme": "Tải mẫu có sẵn", + "inputRadius": "Chá»— nháºp và o", + "checkboxRadius": "Há»™p kiểm", + "instance_default": "(mặc định: {value})", + "instance_default_simple": "(mặc định)", + "interface": "Giao diện", + "interfaceLanguage": "Ngôn ngữ", + "limited_availability": "Trình duyệt không há»— trợ", + "links": "Liên kết", + "lock_account_description": "Tá»± phê duyệt yêu cầu theo dõi", + "loop_video": "Lặp lại video", + "loop_video_silent_only": "Chỉ lặp lại những video không có âm thanh", + "mutes_tab": "Ẩn", + "play_videos_in_modal": "Phát video trong khung hình riêng", + "file_export_import": { + "backup_restore": "Sao lÆ°u", + "backup_settings": "Thiết láºp sao lÆ°u", + "restore_settings": "Khôi phục thiết láºp từ táºp tin", + "errors": { + "invalid_file": "Táºp tin đã chá»n không há»— trợ bởi Pleroma. Giữ nguyên má»i thay đổi.", + "file_too_old": "Phiên bản không tÆ°Æ¡ng thÃch: {fileMajor}, phiên bản táºp tin quá cÅ© và không được há»— trợ (min. set. ver. {feMajor})", + "file_slightly_new": "Phiên bản táºp tin khác biệt, không thể áp dụng má»™t và i thay đổi", + "file_too_new": "Phiên bản không tÆ°Æ¡ng thÃch: {fileMajor}, phiên bản PleromaFE(settings ver {feMajor}) của máy chủ nà y quá cÅ© để sá» dụng" + }, + "backup_settings_theme": "Thiết láºp sao lÆ°u dữ liệu và giao diện" + }, + "profile_fields": { + "label": "Metadata", + "add_field": "Thêm mục", + "name": "Nhãn", + "value": "Ná»™i dung" + }, + "use_contain_fit": "Không cắt ảnh Ä‘Ãnh kèm trong bản xem trÆ°á»›c", + "name": "Tên", + "name_bio": "Tên & tiểu sá»", + "new_email": "Email má»›i", + "new_password": "Máºt khẩu má»›i", + "notification_visibility_follows": "Theo dõi", + "notification_visibility_mentions": "Lượt nhắc", + "notification_visibility_repeats": "Chia sẻ", + "notification_visibility_moves": "Chuyển máy chủ", + "notification_visibility_emoji_reactions": "TÆ°Æ¡ng tác", + "no_blocks": "Không có chặn", + "no_mutes": "Không có ẩn", + "hide_follows_description": "Ẩn danh sách những ngÆ°á»i tôi theo dõi", + "hide_followers_description": "Ẩn danh sách những ngÆ°á»i theo dõi tôi", + "hide_followers_count_description": "Ẩn số lượng ngÆ°á»i theo dõi tôi", + "show_admin_badge": "Hiện huy hiệu \"Quản trị viên\" trên trang của tôi", + "show_moderator_badge": "Hiện huy hiệu \"Kiểm duyệt viên\" trên trang của tôi", + "oauth_tokens": "OAuth tokens", + "token": "Token", + "refresh_token": "Là m tÆ°Æ¡i token", + "valid_until": "Có giá trị tá»›i", + "revoke_token": "Gỡ", + "panelRadius": "Panels", + "pause_on_unfocused": "Dừng phát khi Ä‘ang lÆ°á»›t các tút khác", + "presets": "Mẫu có sẵn", + "profile_background": "Ảnh ná»n trang cá nhân", + "profile_banner": "Ảnh bìa trang cá nhân", + "profile_tab": "Trang cá nhân", + "radii_help": "Thiết láºp góc bo tròn (bằng pixels)", + "replies_in_timeline": "Trả lá»i trong bảng tin", + "reply_visibility_all": "Hiện toà n bá»™ trả lá»i", + "reply_visibility_self": "Chỉ hiện những trả lá»i có nhắc tá»›i tôi", + "reply_visibility_following_short": "Hiện trả lá»i có những ngÆ°á»i tôi theo dõi", + "reply_visibility_self_short": "Hiện trả lá»i của bản thân", + "setting_changed": "Thiết láºp khác vá»›i mặc định", + "block_export_button": "Xuất danh sách chặn ra táºp tin CSV", + "blocks_imported": "Äã nháºp danh sách chặn! Sẽ mất má»™t lúc nữa để hoà n thà nh.", + "cGreen": "Green (Chia sẻ)", + "change_password_error": "Có lá»—i xảy ra khi đổi máºt khẩu.", + "confirm_new_password": "Xác nháºn máºt khẩu má»›i", + "delete_account_description": "Xóa vÄ©nh viá»…n má»i dữ liệu và vô hiệu hóa tà i khoản của bạn.", + "discoverable": "Hiện tà i khoản trong công cụ tìm kiếm và những tÃnh năng khác", + "follow_export_button": "Xuất danh sách theo dõi ra táºp tin CSV", + "hide_attachments_in_tl": "Ẩn táºp tin Ä‘Ãnh kèm trong bảng tin", + "right_sidebar": "Hiện thanh bên bên phải", + "hide_post_stats": "Ẩn tÆ°Æ¡ng tác của tút (vd: số lượt thÃch)", + "import_blocks_from_a_csv_file": "Nháºp danh sách chặn từ táºp tin CSV", + "invalid_theme_imported": "Táºp tin đã chá»n không há»— trợ bởi Pleroma. Giao diện của bạn sẽ giữ nguyên.", + "notification_visibility": "Những loại thông báo sẽ hiện", + "notification_visibility_likes": "ThÃch", + "no_rich_text_description": "Không hiện rich text trong các tút", + "hide_follows_count_description": "Ẩn số lượng ngÆ°á»i tôi theo dõi", + "nsfw_clickthrough": "Cho phép nhấn và o xem các tút nhạy cảm", + "reply_visibility_following": "Chỉ hiện những trả lá»i có nhắc tá»›i tôi hoặc từ những ngÆ°á»i mà tôi theo dõi", + "autohide_floating_post_button": "Ẩn nút viết tút khi xem bảng tin (di Ä‘á»™ng)", + "saving_err": "Thiết láºp lá»—i lÆ°u", + "saving_ok": "Äã lÆ°u các thay đổi", + "search_user_to_block": "Tìm ngÆ°á»i bạn muốn chặn", + "search_user_to_mute": "Tìm ngÆ°á»i bạn muốn ẩn", + "security_tab": "Bảo máºt", + "scope_copy": "Chép phạm vi khi trả lá»i (tin nhắn luôn được chép sẵn)", + "minimal_scopes_mode": "Tùy chá»n thu nhá» phạm vi tút", + "set_new_avatar": "Äổi ảnh đại diện", + "set_new_profile_background": "Äổi ảnh ná»n", + "set_new_profile_banner": "Äổi ảnh bìa", + "reset_profile_background": "Äặt lại ảnh ná»n", + "reset_profile_banner": "Äặt lại ảnh bìa", + "reset_banner_confirm": "Bạn có chắc chắn muốn đặt lại ảnh bìa?", + "reset_background_confirm": "Bạn có chắc chắn muốn đặt lại ảnh ná»n?", + "settings": "Cà i đặt", + "subject_input_always_show": "Luôn hiện vùng tiêu Ä‘á»", + "subject_line_behavior": "Chép tiêu Ä‘á» khi trả lá»i", + "subject_line_email": "Giống email: \"re: subject\"", + "subject_line_mastodon": "Giống Mastodon: copy as is", + "subject_line_noop": "Äừng chép", + "sensitive_by_default": "Mặc định tút là nhạy cảm", + "stop_gifs": "Chỉ phát GIF khi chạm và o", + "streaming": "Tá»± Ä‘á»™ng tải tút má»›i khi cuá»™n lên trên", + "user_mutes": "NgÆ°á»i dùng", + "useStreamingApiWarning": "(TÃnh năng thá» nghiệm, không Ä‘á» xuất sá» dụng)", + "text": "Văn bản", + "theme": "Theme", + "theme_help": "Dùng mã mà u hex (#rrggbb) để tá»± chế theme.", + "tooltipRadius": "Tooltips/alerts", + "type_domains_to_mute": "Tìm máy chủ để ẩn", + "upload_a_photo": "Tải ảnh lên", + "user_settings": "Thiết láºp ngÆ°á»i dùng", + "values": { + "false": "không", + "true": "có" + }, + "virtual_scrolling": "Render bảng tin", + "fun": "Vui nhá»™n", + "greentext": "MÅ©i tên meme", + "notifications": "Thông báo", + "notification_setting_filters": "Bá»™ lá»c", + "notification_setting_block_from_strangers": "Chặn thông báo từ những ngÆ°á»i bạn không theo dõi", + "notification_setting_privacy": "Riêng tÆ°", + "notification_setting_hide_notification_contents": "Ẩn ngÆ°á»i gá»i và ná»™i dung thông báo đẩy", + "notification_mutes": "Sá» dụng ẩn nếu muốn dừng nháºn thông báo từ má»™t ngÆ°á»i cụ thể.", + "notification_blocks": "Chặn má»™t ngÆ°á»i ngừng toà n bá»™ thông báo cÅ©ng giống nhÆ° hủy đăng ký há».", + "more_settings": "Cà i đặt khác", + "style": { + "switcher": { + "keep_shadows": "Giữ bóng đổ", + "keep_color": "Giữ mà u", + "keep_opacity": "Giữ trong suốt", + "keep_roundness": "Giữ bo tròn góc", + "reset": "Äặt lại", + "clear_all": "Xóa hết", + "clear_opacity": "Xóa trong suốt", + "load_theme": "Tải theme", + "keep_as_is": "Giữ nhÆ° là ", + "use_snapshot": "Bản cÅ©", + "use_source": "Bản má»›i", + "help": { + "upgraded_from_v2": "PleromaFE đã được nâng cấp, theme có thể khác hÆ¡n má»™t chút so vá»›i bản cÅ©.", + "v2_imported": "Táºp tin bạn nháºp là từ phiên bản PleromaFE cÅ©. Chúng tôi sẽ cố là m nó tÆ°Æ¡ng thÃch nhÆ°ng có thể sẽ có xung Ä‘á»™t.", + "older_version_imported": "Táºp tin bạn vừa nháºp được tạo ra từ phiên bản PleromaFE cÅ©.", + "snapshot_present": "Äã tải theme snapshot, má»i giá trị sẽ bị chép đè. Thay và o đó, bạn có thể tải dữ liệu chắc chắn của theme.", + "fe_upgraded": "Theme của PleromaFE được nâng cấp sau má»—i phiên bản.", + "fe_downgraded": "Theme của phiên bản PleromaFE đã được hạ cấp.", + "migration_snapshot_ok": "Theme snapshot đã tải xong. Bạn có thể thá» tải dữ liệu theme.", + "migration_napshot_gone": "Nếu thiếu snapshot, má»™t số thứ sẽ khác vá»›i ban đầu.", + "future_version_imported": "Táºp tin bạn vừa nháºp được tạo ra từ phiên bản PleromaFE má»›i.", + "snapshot_missing": "Không có theme snapshot trong táºp tin cho nên có thể nó sẽ khác vá»›i bản gốc đôi chút.", + "snapshot_source_mismatch": "Xung Ä‘á»™t phiên bản: hầu hết Pleroma FE đã hạ cấp và cáºp nháºt lại, nếu bạn đổi theme sá» dụng phiên bản cÅ© hÆ¡n của FE, bạn gần nhÆ° muốn sá» dụng phiên bản cÅ©, thay và o đó sá» dụng phiên bản má»›i." + }, + "keep_fonts": "Giữ phông chữ", + "save_load_hint": "Giúp giữ nguyên các tùy chá»n hiện tại khi chá»n hoặc tải theme khác, nó cÅ©ng lÆ°u trữ các tùy chá»n đã nói khi xuất má»™t theme. Khi tất cả các há»™p kiểm bị bá» trống, việc xuất theme sẽ lÆ°u má»i thứ." + }, + "common": { + "color": "Mà u sắc", + "opacity": "Trong suốt", + "contrast": { + "hint": "Tỉ lệ tÆ°Æ¡ng phản là {ratio}, nó {level} {context}", + "level": { + "aa": "đạt mức AA (tối thiểu)", + "aaa": "đạt mức AAA (Ä‘á» xuất)", + "bad": "không đạt yêu cầu" + }, + "context": { + "18pt": "cỡ chữ lá»›n (18pt+)", + "text": "cho chữ" + } + } + }, + "common_colors": { + "_tab_label": "Chung", + "main": "Mà u sắc chung", + "foreground_hint": "Mở tab \"Nâng cao\" để có nhiá»u tùy chá»n hÆ¡n", + "rgbo": "Icons, accents, badges" + }, + "advanced_colors": { + "_tab_label": "Nâng cao", + "alert": "Ná»n cảnh báo", + "alert_error": "Lá»—i", + "alert_warning": "Cảnh báo", + "alert_neutral": "Neutral", + "post": "Tút/Tiểu sá»", + "badge": "Ná»n huy hiệu", + "popover": "Tooltips, menus, popovers", + "badge_notification": "Thông báo", + "panel_header": "Tiêu Ä‘á» panel", + "top_bar": "Thanh trên cùng", + "borders": "ÄÆ°á»ng biên", + "buttons": "Nút bấm", + "faint_text": "Chữ má»", + "underlay": "Lá»›p dÆ°á»›i", + "wallpaper": "Wallpaper", + "poll": "Biểu đồ cuá»™c bình chá»n", + "icons": "Biểu tượng", + "highlight": "Những thà nh phần nổi báºt", + "pressed": "Khi nhấn xuống", + "selectedPost": "Chá»n tút", + "selectedMenu": "Chá»n menu", + "toggled": "Toggled", + "tabs": "Tab", + "chat": { + "incoming": "Tin nhắn đến", + "outgoing": "Tin nhắn Ä‘i", + "border": "ÄÆ°á»ng biên" + }, + "inputs": "Khung soạn thảo", + "disabled": "Vô hiệu hóa" + }, + "radii": { + "_tab_label": "Góc bo tròn" + }, + "shadows": { + "component": "Thà nh phần", + "shadow_id": "Äổ bóng #{value}", + "blur": "Là m má»", + "spread": "Mở rá»™ng", + "inset": "Thu và o", + "filter_hint": { + "always_drop_shadow": "Chú ý, mà u bóng đổ nà y luôn sá» dụng {0} nếu trình duyệt há»— trợ.", + "drop_shadow_syntax": "{0} không há»— trợ {1} phần và từ khóa {2}.", + "spread_zero": "Bóng đổ > 0 sẽ xuất hiện nếu chá»n nó thà nh không", + "inset_classic": "Bóng đổ inset sẽ sá» dụng {0}", + "avatar_inset": "Nếu trá»™n lẫn bóng đổ inset và non-inset trên ảnh đại diện có thể khiến ảnh đại diện biến thà nh trong suốt." + }, + "components": { + "panel": "Panel", + "panelHeader": "Panel ảnh bìa", + "topBar": "Thanh trên cùng", + "avatar": "Ảnh đại diện (ở trang cá nhân)", + "avatarStatus": "Ảnh đại diện (ở tút)", + "popup": "Popups và tooltips", + "button": "Nút bấm", + "buttonHover": "Nút bấm (khi rê chuá»™t)", + "buttonPressed": "Nút bấm (khi nhấn chuá»™t)", + "buttonPressedHover": "Nút bấm (khi nhấn+giữ)", + "input": "Khung soạn thảo" + }, + "_tab_label": "Äổ bóng và tô sáng", + "override": "Chép đè", + "hintV3": "Vá»›i bóng đổ, bạn có thể sá» dụng ký hiệu {0} để dùng slot mà u khác." + }, + "fonts": { + "_tab_label": "Phông chữ", + "components": { + "interface": "Giao diện chung", + "input": "Khung soạn thảo", + "post": "Tút", + "postCode": "Chữ monospaced (rich text)" + }, + "family": "Tên phông", + "size": "KÃch cỡ (px)", + "weight": "Äá»™ Ä‘áºm", + "custom": "Tùy chỉnh", + "help": "Chá»n phông chữ hiển thị. Äể \"tùy chá»n\", bạn phải nháºp chÃnh xác tên phông chữ trên hệ thống." + }, + "preview": { + "header": "Xem trÆ°á»›c", + "content": "Ná»™i dung", + "error": "Lá»—i mẫu và dụ", + "button": "Nút bấm", + "text": "Má»™t đống {0} và {1}", + "mono": "ná»™i dung", + "input": "Äá»i ngÆ°á»i con gái không muốn yêu ai được không?", + "faint_link": "tà i liệu hÆ°á»›ng dẫn", + "checkbox": "Tôi đã Ä‘á»c lÆ°á»›t qua quy tắc và chÃnh sách bảo máºt", + "link": "Link đẹp đó em yêu", + "fine_print": "Äá»c {0} để tìm hiểu thêm!", + "header_faint": "OK nè" + } + }, + "version": { + "title": "Phiên bản", + "frontend_version": "Frontend", + "backend_version": "Backend" + }, + "reset_avatar": "Äặt lại ảnh đại diện", + "reset_avatar_confirm": "Bạn có chắc chắn muốn đặt lại ảnh đại diện?", + "post_status_content_type": "Loại tút đăng", + "useStreamingApi": "Nháºn tút và thông báo theo thá»i gian thá»±c", + "theme_help_v2_1": "Bạn cÅ©ng có thể xóa hết mà u thà nh phần và là m theme trong suốt, chá»n nút \"Xóa hết\".", + "theme_help_v2_2": "Các biểu tượng bên dÆ°á»›i các mục có Ä‘á»™ tÆ°Æ¡ng phản ná»n/văn bản, hãy rê chuá»™t qua để biết thông tin chi tiết. Xin lÆ°u ý rằng, khi sá» dụng các Ä‘á»™ tÆ°Æ¡ng phản trong suốt có thể khiến Ä‘á»c chữ không ra.", + "enable_web_push_notifications": "Cho phép thông báo đẩy trên web", + "mentions_new_style": "Lượt nhắc mà u mè", + "mentions_new_place": "Äặt lượt nhắc ở dòng riêng", + "always_show_post_button": "Luôn hiện nút viết tút má»›i" + }, + "errors": { + "storage_unavailable": "Pleroma không thể truy cáºp lÆ°u trữ trình duyệt. Thông tin đăng nháºp và những thiết láºp tạm thá»i sẽ bị mất. Hãy cho phép cookies." + }, + "time": { + "day": "{0} ngà y", + "days": "{0} ngà y", + "day_short": "{0} ngà y", + "days_short": "{0} ngà y", + "hour": "{0} giá»", + "hours": "{0} giá»", + "hour_short": "{0} giá»", + "hours_short": "{0} giá»", + "in_future": "lúc {0}", + "in_past": "{0} trÆ°á»›c", + "minute": "{0} phút", + "minutes": "{0} phút", + "minute_short": "{0} phút", + "minutes_short": "{0} phút", + "month": "{0} tháng", + "months": "{0} tháng", + "month_short": "{0} tháng", + "months_short": "{0} tháng", + "now": "vừa xong", + "second": "{0} giây", + "seconds": "{0} giây", + "second_short": "{0}s", + "seconds_short": "{0}s", + "week": "{0} tuần", + "weeks": "{0} tuần", + "week_short": "{0} tuần", + "weeks_short": "{0} tuần", + "year": "{0} năm", + "years": "{0} năm", + "year_short": "{0} năm", + "years_short": "{0} năm", + "now_short": "vừa xong" + }, + "timeline": { + "collapse": "Thu gá»n", + "error": "Lá»—i khi nạp bảng tin {0}", + "load_older": "Xem tút cÅ© hÆ¡n", + "repeated": "chia sẻ", + "show_new": "Hiện má»›i", + "reload": "Tải lại", + "up_to_date": "Äã tải những tút má»›i nhất", + "no_more_statuses": "Không còn tút nà o", + "no_statuses": "Trống trÆ¡n!", + "socket_reconnected": "Thiết láºp kết nối thá»i gian thá»±c", + "conversation": "Thảo luáºn", + "no_retweet_hint": "Không thể chia sẻ tin nhắn và những tút riêng tÆ°", + "socket_broke": "Mất kết nối thá»i gian thá»±c: CloseEvent {0}" + }, + "status": { + "repeats": "Chia sẻ", + "delete": "Xóa tút", + "unpin": "Bá» ghim trên trang cá nhân", + "pin": "Ghim trên trang cá nhân", + "pinned": "Tút được ghim", + "bookmark": "LÆ°u", + "unbookmark": "Bá» lÆ°u", + "reply_to": "Trả lá»i", + "replies_list": "Những trả lá»i:", + "mute_conversation": "Không quan tâm nữa", + "unmute_conversation": "Quan tâm", + "status_unavailable": "Không tìm thấy tút", + "copy_link": "Sao chép URL", + "external_source": "Nguồn bên ngoà i", + "thread_muted": "Äã ẩn chủ Ä‘á»", + "thread_muted_and_words": ", có từ:", + "hide_full_subject": "Ẩn tiêu Ä‘á»", + "show_content": "Hiện ná»™i dung", + "hide_content": "Ẩn ná»™i dung", + "status_deleted": "Tút nà y đã bị xóa", + "nsfw": "Nhạy cảm", + "expand": "Xem nguyên văn", + "favorites": "ThÃch", + "delete_confirm": "Bạn có chắc chắn muốn xóa tút nà y?", + "show_full_subject": "Hiện đầy đủ tiêu Ä‘á»", + "you": "(Bạn)", + "mentions": "Lượt nhắc", + "plus_more": "+{number} nhiá»u hÆ¡n" + }, + "user_card": { + "approve": "Chấp nháºn", + "block": "Chặn", + "blocked": "Äã chặn!", + "deny": "Từ chối", + "edit_profile": "Chỉnh sá»a trang cá nhân", + "favorites": "ThÃch", + "follow": "Theo dõi", + "follow_progress": "Äang yêu cầu…", + "follow_again": "Gá»i lại yêu cầu?", + "follow_unfollow": "NgÆ°ng theo dõi", + "followees": "Äang theo dõi", + "followers": "NgÆ°á»i theo dõi", + "following": "Äang theo dõi!", + "follows_you": "Theo dõi bạn!", + "hidden": "Ẩn", + "media": "Media", + "mention": "Lượt nhắc", + "message": "Tin nhắn", + "mute": "Ẩn", + "muted": "Äã ẩn", + "per_day": "tút má»—i ngà y", + "remote_follow": "Theo dõi từ xa", + "report": "Báo cáo", + "statuses": "Tút", + "subscribe": "Äăng ký", + "unsubscribe": "Hủy đăng ký", + "unblock": "Bá» chặn", + "unblock_progress": "Äang bá» chặn…", + "block_progress": "Äang chặn…", + "unmute": "BỠẩn", + "unmute_progress": "Äang bỠẩn…", + "mute_progress": "Äang ẩn…", + "hide_repeats": "Ẩn lượt chia sẻ", + "show_repeats": "Hiện lượt chia sẻ", + "bot": "Bot", + "admin_menu": { + "moderation": "Kiểm duyệt", + "grant_admin": "Chỉ định Quản trị viên", + "revoke_admin": "Gỡ bá» Quản trị viên", + "grant_moderator": "Chỉ định Kiểm duyệt viên", + "activate_account": "Xác thá»±c ngÆ°á»i dùng", + "deactivate_account": "Vô hiệu hóa ngÆ°á»i dùng", + "delete_account": "Xóa ngÆ°á»i dùng", + "force_nsfw": "Äánh dấu tất cả tút là nhạy cảm", + "strip_media": "Gỡ bá» media trong tút", + "sandbox": "Äánh dấu tất cả tút là riêng tÆ°", + "disable_remote_subscription": "Không cho phép theo dõi từ máy chủ khác", + "disable_any_subscription": "Không cho phép theo dõi bất cứ ai", + "quarantine": "Không cho phép tút liên hợp", + "delete_user": "Xóa ngÆ°á»i dùng", + "revoke_moderator": "Gỡ bá» Quản trị viên", + "force_unlisted": "Äánh dấu tất cả tút là hạn chế", + "delete_user_confirmation": "Bạn chắc chắn chÆ°a? Hà nh Ä‘á»™ng nà y không thể phục hồi." + }, + "highlight": { + "disabled": "Không nổi báºt", + "solid": "Ná»n 1 mà u", + "striped": "Ná»n 2 mà u", + "side": "Sá»c bên" + }, + "follow_sent": "Äã gá»i yêu cầu!", + "its_you": "Äó là bạn!" + }, + "user_profile": { + "timeline_title": "Bảng tin ngÆ°á»i dùng", + "profile_does_not_exist": "Xin lá»—i, tà i khoản nà y không tồn tại.", + "profile_loading_error": "Xin lá»—i, có lá»—i xảy ra khi xem trang cá nhân nà y." + }, + "user_reporting": { + "title": "Báo cáo {0}", + "additional_comments": "Ghi chú", + "forward_description": "NgÆ°á»i nà y thuá»™c máy chủ khác. Gá»i má»™t báo cáo ẩn danh tá»›i máy chủ đó?", + "forward_to": "Chuyển cho {0}", + "submit": "Gá»i", + "generic_error": "Có lá»—i xảy ra khi xá» lý yêu cầu của bạn.", + "add_comment_description": "Hãy cho quản trị viên biết lý do vì sao bạn báo cáo ngÆ°á»i nà y:" + }, + "who_to_follow": { + "more": "Nhiá»u hÆ¡n nữa", + "who_to_follow": "Những ngÆ°á»i dùng nổi báºt" + }, + "tool_tip": { + "media_upload": "Tải lên media", + "repeat": "Chia sẻ", + "reply": "Trả lá»i", + "favorite": "ThÃch", + "add_reaction": "Thêm tÆ°Æ¡ng tác", + "accept_follow_request": "Phê duyệt yêu cầu theo dõi", + "reject_follow_request": "Từ chối yêu cầu theo dõi", + "bookmark": "LÆ°u", + "user_settings": "Thiết láºp ngÆ°á»i dùng" + }, + "upload": { + "error": { + "base": "Tải lên thất bại.", + "message": "Tải lên thất bại: {0}", + "file_too_big": "Táºp tin quá lá»›n [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Hãy thá» lại sau" + }, + "file_size_units": { + "KiB": "KB", + "MiB": "MB", + "GiB": "GB", + "B": "byte", + "TiB": "TB" + } + }, + "search": { + "people": "NgÆ°á»i", + "hashtags": "Hashtag", + "person_talking": "{count} ngÆ°á»i Ä‘ang trò chuyện", + "people_talking": "{count} ngÆ°á»i Ä‘ang trò chuyện", + "no_results": "Không tìm thấy" + }, + "password_reset": { + "forgot_password": "Quên máºt khẩu", + "password_reset": "Äổi máºt khẩu", + "placeholder": "Email hoặc tên ngÆ°á»i dùng", + "check_email": "Kiểm tra email của bạn.", + "return_home": "Quay lại Pleroma", + "too_many_requests": "Bạn đã vượt giá»›i hạn cho phép, hãy thá» lại sau.", + "password_reset_disabled": "Reset máºt khẩu bị tắt. Hãy liên hệ quản trị viên máy chủ.", + "password_reset_required": "Bạn phải đổi máºt khẩu để đăng nháºp.", + "instruction": "Nháºp email hoặc tên ngÆ°á»i dùng. Chúng tôi sẽ gá»i email reset máºt khẩu cho bạn.", + "password_reset_required_but_mailer_is_disabled": "Bạn cần phải đổi máºt khẩu, nhÆ°ng tÃnh năng bị tắt. Hãy liên hệ quản trị viên máy chủ." + }, + "chats": { + "you": "Bạn:", + "message_user": "Nhắn tin {nickname}", + "delete": "Xóa", + "chats": "Chat", + "new": "Chat má»›i", + "empty_message_error": "Không thể gá»i tin nhắn trống", + "more": "Nhiá»u hÆ¡n", + "delete_confirm": "Bạn có chắc chắn muốn xóa tin nhắn nà y?", + "error_loading_chat": "Có vấn Ä‘á» khi tải giao diện chat.", + "error_sending_message": "Có vấn Ä‘á» khi gá»i tin nhắn.", + "empty_chat_list_placeholder": "Bạn không có tin nhắn. Hãy bắt đầu nhắn cho ai đó!" + }, + "file_type": { + "audio": "Âm thanh", + "video": "Video", + "image": "Hình ảnh", + "file": "Táºp tin" + }, + "display_date": { + "today": "Hôm nay" + } +} diff --git a/src/i18n/zh.json b/src/i18n/zh.json index b4185b30506ca0db85f1f88c2792881fd04cac83..abba4be9339005f58c270a994202d2600f8a90ed 100644 --- a/src/i18n/zh.json +++ b/src/i18n/zh.json @@ -39,7 +39,14 @@ "close": "å…³é—", "retry": "é‡è¯•", "error_retry": "请é‡è¯•", - "loading": "载入ä¸â€¦" + "loading": "载入ä¸â€¦", + "role": { + "moderator": "监察员", + "admin": "管ç†å‘˜" + }, + "flash_content": "点击以使用 Ruffle 显示 Flash 内容(实验性,å¯èƒ½æ— 效)。", + "flash_security": "注æ„è¿™å¯èƒ½æœ‰æ½œåœ¨çš„å±é™©ï¼Œå› 为 Flash 内容ä»ç„¶æ˜¯ä»»æ„的代ç 。", + "flash_fail": "Flash å†…å®¹åŠ è½½å¤±è´¥ï¼Œè¯·åœ¨æŽ§åˆ¶å°æŸ¥çœ‹è¯¦æƒ…。" }, "image_cropper": { "crop_picture": "è£å‰ªå›¾ç‰‡", @@ -92,7 +99,8 @@ "administration": "管ç†å‘˜", "chats": "èŠå¤©", "timelines": "时间线", - "bookmarks": "书ç¾" + "bookmarks": "书ç¾", + "home_timeline": "主页时间线" }, "notifications": { "broken_favorite": "未知的状æ€ï¼Œæ£åœ¨æœç´¢ä¸â€¦", @@ -120,7 +128,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": "æ·»åŠ è´´çº¸" @@ -162,7 +172,8 @@ "preview": "预览", "media_description": "媒体æè¿°", "media_description_error": "更新媒体失败,请é‡è¯•", - "empty_status_error": "ä¸èƒ½å‘布没有内容ã€æ²¡æœ‰é™„件的å‘æ–‡" + "empty_status_error": "ä¸èƒ½å‘布没有内容ã€æ²¡æœ‰é™„件的å‘æ–‡", + "post": "å‘é€" }, "registration": { "bio": "简介", @@ -183,7 +194,10 @@ "password_required": "ä¸èƒ½ç•™ç©º", "password_confirmation_required": "ä¸èƒ½ç•™ç©º", "password_confirmation_match": "密ç ä¸ä¸€è‡´" - } + }, + "reason_placeholder": "æ¤å®žä¾‹çš„注册需è¦æ‰‹åŠ¨æ‰¹å‡†ã€‚\n请让管ç†å‘˜çŸ¥é“您为什么想è¦æ³¨å†Œã€‚", + "reason": "注册ç†ç”±", + "register": "注册" }, "selectable_list": { "select_all": "选择全部" @@ -290,7 +304,7 @@ "new_password": "新密ç ", "notification_visibility": "è¦æ˜¾ç¤ºçš„通知类型", "notification_visibility_follows": "关注", - "notification_visibility_likes": "点赞", + "notification_visibility_likes": "喜欢", "notification_visibility_mentions": "æåŠ", "notification_visibility_repeats": "转å‘", "no_rich_text_description": "ä¸æ˜¾ç¤ºå¯Œæ–‡æœ¬æ ¼å¼", @@ -298,8 +312,8 @@ "no_mutes": "没有éšè—", "hide_follows_description": "ä¸è¦æ˜¾ç¤ºæˆ‘所关注的人", "hide_followers_description": "ä¸è¦æ˜¾ç¤ºå…³æ³¨æˆ‘的人", - "show_admin_badge": "在我的个人资料ä¸æ˜¾ç¤ºç®¡ç†å‘˜å¾½ç« ", - "show_moderator_badge": "在我的个人资料ä¸æ˜¾ç¤ºç›‘å¯Ÿå‘˜å¾½ç« ", + "show_admin_badge": "在我的个人资料ä¸æ˜¾ç¤ºâ€œç®¡ç†å‘˜â€å¾½ç« ", + "show_moderator_badge": "在我的个人资料ä¸æ˜¾ç¤ºâ€œç›‘察员â€å¾½ç« ", "nsfw_clickthrough": "å°†ä¸å’Œè°é™„件和链接预览éšè—,点击æ‰ä¼šæ˜¾ç¤º", "oauth_tokens": "OAuth令牌", "token": "令牌", @@ -552,7 +566,30 @@ "mute_import": "éšè—åå•å¯¼å…¥", "mute_export_button": "å¯¼å‡ºä½ çš„éšè—åå•åˆ°ä¸€ä¸ª csv 文件", "mute_export": "éšè—åå•å¯¼å‡º", - "hide_wallpaper": "éšè—实例å£çº¸" + "hide_wallpaper": "éšè—实例å£çº¸", + "setting_changed": "与默认设置ä¸åŒ", + "more_settings": "更多设置", + "sensitive_by_default": "é»˜è®¤æ ‡è®°å‘文为æ•æ„Ÿå†…容", + "reply_visibility_self_short": "åªæ˜¾ç¤ºå¯¹æˆ‘本人的回å¤", + "reply_visibility_following_short": "显示对我关注的人的回å¤", + "hide_all_muted_posts": "ä¸æ˜¾ç¤ºå·²éšè—çš„å‘æ–‡", + "hide_media_previews": "éšè—媒体预览", + "word_filter": "è¯è¯è¿‡æ»¤", + "save": "ä¿å˜æ›´æ”¹", + "file_export_import": { + "errors": { + "file_slightly_new": "文件的å°ç‰ˆæœ¬ä¸åŒï¼Œæœ‰äº›è®¾ç½®å¯èƒ½æ— æ³•åŠ è½½", + "file_too_old": "ä¸å…¼å®¹çš„主版本:{fileMajor},文件版本过旧,ä¸å—支æŒï¼ˆæœ€å°è®¾ç½®ç‰ˆæœ¬ {feMajor})", + "file_too_new": "ä¸å…¼å®¹çš„主版本:{fileMajor}ï¼Œæ¤ PleromaFE(设置版本 {feMajor}ï¼‰è¿‡æ—§ï¼Œæ— æ³•å¤„ç†", + "invalid_file": "所选文件ä¸æ˜¯å—支æŒçš„ Pleroma 设置备份。没有进行任何更改。" + }, + "restore_settings": "从文件æ¢å¤è®¾ç½®", + "backup_settings_theme": "备份设置和主题到文件", + "backup_settings": "备份设置到文件", + "backup_restore": "设置备份" + }, + "right_sidebar": "在å³ä¾§æ˜¾ç¤ºä¾§è¾¹æ ", + "hide_shoutbox": "éšè—实例留言æ¿" }, "time": { "day": "{0} 天", @@ -600,7 +637,9 @@ "no_more_statuses": "没有更多的状æ€", "no_statuses": "没有状æ€æ›´æ–°", "reload": "é‡æ–°è½½å…¥", - "error": "å–得时间轴时å‘生错误:{0}" + "error": "å–得时间轴时å‘生错误:{0}", + "socket_broke": "丢失实时连接:CloseEvent code {0}", + "socket_reconnected": "已建立实时连接" }, "status": { "favorites": "喜欢", @@ -638,7 +677,6 @@ "follow": "关注", "follow_sent": "请求已å‘é€ï¼", "follow_progress": "请求ä¸â€¦", - "follow_again": "å†æ¬¡å‘é€è¯·æ±‚?", "follow_unfollow": "å–消关注", "followees": "æ£åœ¨å…³æ³¨", "followers": "关注者", @@ -683,7 +721,15 @@ "show_repeats": "显示转å‘", "hide_repeats": "éšè—转å‘", "message": "消æ¯", - "mention": "æåŠ" + "mention": "æåŠ", + "bot": "机器人", + "highlight": { + "side": "侧边æ¡çº¹", + "striped": "æ¡çº¹èƒŒæ™¯", + "solid": "å•ä¸€é¢œè‰²èƒŒæ™¯", + "disabled": "ä¸çªå‡ºæ˜¾ç¤º" + }, + "edit_profile": "编辑个人资料" }, "user_profile": { "timeline_title": "用户时间线", @@ -778,8 +824,8 @@ "media_nsfw_desc": "本实例将æ¥è‡ªä»¥ä¸‹å®žä¾‹çš„媒体内容强制设置为æ•æ„Ÿå†…容:", "media_nsfw": "强制设置媒体为æ•æ„Ÿå†…容", "media_removal_desc": "本实例移除æ¥è‡ªä»¥ä¸‹å®žä¾‹çš„媒体内容:", - "ftl_removal_desc": "该实例在从“全部已知网络â€æ—¶é—´çº¿ä¸Šç§»é™¤äº†ä¸‹åˆ—实例:", - "ftl_removal": "从“全部已知网络â€æ—¶é—´çº¿ä¸Šç§»é™¤" + "ftl_removal_desc": "该实例在从“已知网络â€æ—¶é—´çº¿ä¸Šç§»é™¤äº†ä¸‹åˆ—实例:", + "ftl_removal": "从“已知网络â€æ—¶é—´çº¿ä¸Šç§»é™¤" }, "mrf_policies_desc": "MRF ç–略会影å“本实例的互通行为。以下ç–略已å¯ç”¨ï¼š", "mrf_policies": "å·²å¯ç”¨çš„ MRF ç–ç•¥", @@ -801,7 +847,7 @@ "mute": "éšè—" }, "errors": { - "storage_unavailable": "Pleroma æ— æ³•è®¿é—®æµè§ˆå™¨å‚¨å˜ã€‚您的登陆å以åŠæœ¬åœ°è®¾ç½®å°†ä¸ä¼šè¢«ä¿å˜ï¼Œæ‚¨å¯èƒ½é‡åˆ°æ„外问题。请å°è¯•å¯ç”¨ cookies。" + "storage_unavailable": "Pleroma æ— æ³•è®¿é—®æµè§ˆå™¨å‚¨å˜ã€‚您的登陆以åŠæœ¬åœ°è®¾ç½®å°†ä¸ä¼šè¢«ä¿å˜ï¼Œæ‚¨ä¹Ÿå¯èƒ½é‡åˆ°æœªçŸ¥é—®é¢˜ã€‚请å°è¯•å¯ç”¨ cookies。" }, "shoutbox": { "title": "留言æ¿" diff --git a/src/i18n/zh_Hant.json b/src/i18n/zh_Hant.json index a8d0dc3c90232e06bb6fa4d21eb1cd3cee43eabf..2c4dc3fb31a95349ef958db47592f2ebb48b19e3 100644 --- a/src/i18n/zh_Hant.json +++ b/src/i18n/zh_Hant.json @@ -22,10 +22,12 @@ "votes": "票", "option": "é¸é …", "add_option": "å¢žåŠ é¸é …", - "add_poll": "å¢žåŠ æŠ•ç¥¨" + "add_poll": "å¢žåŠ æŠ•ç¥¨", + "votes_count": "{count} 票 | {count} 票", + "people_voted_count": "{count} 人已投票 | {count} 人已投票" }, "notifications": { - "reacted_with": "å’Œ {0} 互動éŽ", + "reacted_with": "作出了 {0} çš„å應", "migrated_to": "é·ç§»åˆ°", "no_more_notifications": "沒有更多的通知", "repeated_you": "è½‰ç™¼äº†ä½ çš„ç™¼æ–‡", @@ -54,8 +56,9 @@ "mentions": "æåŠ", "friend_requests": "關注請求", "back": "後退", - "administration": "管ç†", - "about": "關於" + "administration": "管ç†å“¡", + "about": "關於", + "home_timeline": "家時間線" }, "media_modal": { "next": "往後", @@ -108,7 +111,14 @@ "loading": "載入ä¸â€¦", "more": "更多", "submit": "æ交", - "apply": "應用" + "apply": "應用", + "role": { + "moderator": "主æŒäºº", + "admin": "管ç†å“¡" + }, + "flash_content": "點擊以使用 Ruffle 顯示 Flash 內容(實驗性,å¯èƒ½ç„¡æ•ˆï¼‰ã€‚", + "flash_security": "請注æ„,這å¯èƒ½æœ‰æ½œåœ¨çš„å±éšªï¼Œå› 為Flash內容ä»ç„¶æ˜¯æ¦æ–·çš„程å¼ç¢¼ã€‚", + "flash_fail": "ç„¡æ³•åŠ è¼‰flash內容,請åƒé–±æŽ§åˆ¶å°çžè§£è©³ç´°è³‡è¨Šã€‚" }, "finder": { "find_user": "尋找用戶", @@ -216,7 +226,8 @@ "incoming": "收到", "outgoing": "發出", "border": "邊框" - } + }, + "wallpaper": "桌布" }, "preview": { "header_faint": "這很æ£å¸¸", @@ -321,7 +332,7 @@ "notification_visibility_moves": "用戶é·ç§»", "notification_visibility_repeats": "轉發", "notification_visibility_mentions": "æåŠ", - "notification_visibility_likes": "點贊", + "notification_visibility_likes": "å–œæ¡", "interfaceLanguage": "ç•Œé¢èªžè¨€", "instance_default": "(默èªï¼š{value})", "inputRadius": "輸入框", @@ -412,7 +423,7 @@ "hide_follows_description": "ä¸è¦é¡¯ç¤ºæˆ‘所關注的人", "hide_followers_description": "ä¸è¦é¡¯ç¤ºé—œæ³¨æˆ‘的人", "hide_follows_count_description": "ä¸é¡¯ç¤ºé—œæ³¨æ•¸", - "nsfw_clickthrough": "å°‡æ•æ„Ÿé™„件隱è—,點擊æ‰èƒ½æ‰“é–‹", + "nsfw_clickthrough": "å°‡æ•æ„Ÿé™„件和éˆæŽ¥éš±è—,點擊æ‰èƒ½æ‰“é–‹", "valid_until": "有效期至", "panelRadius": "é¢æ¿", "pause_on_unfocused": "在離開é é¢æ™‚æš«åœæ™‚間線推é€", @@ -423,7 +434,7 @@ "notification_blocks": "å°éŽ–一個用戶會åœæŽ‰æ‰€æœ‰ä»–的通知,ç‰åŒæ–¼å–消關注。", "enable_web_push_notifications": "啟用 web 推é€é€šçŸ¥", "presets": "é ç½®", - "profile_background": "個人背景圖", + "profile_background": "é…置文件背景圖", "profile_banner": "橫幅圖片", "profile_tab": "個人資料", "radii_help": "è¨ç½®ç•Œé¢é‚Šç·£çš„圓角 (å–®ä½ï¼šåƒç´ )", @@ -511,7 +522,7 @@ "show_moderator_badge": "顯示主æŒäººå¾½ç« ", "oauth_tokens": "OAuth代幣", "token": "代幣", - "refresh_token": "刷新代幣", + "refresh_token": "刷新token", "useStreamingApiWarning": "(ä¸æŽ¨è–¦ä½¿ç”¨ï¼Œå¯¦é©—性的,已知跳éŽæ–‡ç« )", "fun": "有趣", "notification_setting_hide_notification_contents": "éš±è—推é€é€šçŸ¥ä¸çš„發é€è€…與內容信æ¯", @@ -527,7 +538,30 @@ "mute_import_error": "å°Žå…¥éœéŸ³æ™‚出錯", "mute_export_button": "å°‡éœéŸ³å°Žå‡ºåˆ°csv文件", "mute_export": "éœéŸ³å°Žå‡º", - "hide_wallpaper": "éš±è—實例桌布" + "hide_wallpaper": "éš±è—實例桌布", + "reply_visibility_self_short": "åªé¡¯ç¤ºå°æˆ‘本人的回å¤", + "reply_visibility_following_short": "顯示å°æˆ‘關注的人的回å¤", + "hide_all_muted_posts": "ä¸é¡¯ç¤ºå·²éš±è—的帖å", + "hide_media_previews": "éš±è—媒體é 覽", + "word_filter": "è©žéŽæ¿¾", + "setting_changed": "與默èªè¨ç½®ä¸åŒ", + "more_settings": "更多è¨ç½®", + "save": "ä¿å˜æ›´æ”¹", + "file_export_import": { + "errors": { + "invalid_file": "所é¸æ–‡ä»¶ä¸æ˜¯å—支æŒçš„Pleromaè¨ç½®å‚™ä»½ã€‚ 沒有進行任何更改。", + "file_too_new": "ä¸å…¼å®¹çš„主版本:{fileMajor}ï¼Œæ¤ PleromaFE(è¨ç½®ç‰ˆæœ¬ {feMajor})éŽèˆŠï¼Œç„¡æ³•è™•ç†", + "file_too_old": "ä¸å…¼å®¹çš„主版本:{fileMajor},文件版本éŽèˆŠï¼Œä¸å—支æŒï¼ˆæœ€å°è¨ç½®ç‰ˆæœ¬ {feMajor})", + "file_slightly_new": "檔案的å°ç‰ˆæœ¬ä¸åŒï¼Œæœ‰äº›è¨ç½®å¯èƒ½ç„¡æ³•è¼‰å…¥" + }, + "restore_settings": "從文件還原è¨ç½®", + "backup_settings_theme": "備份è¨ç½®å’Œä¸»é¡Œåˆ°æ–‡ä»¶", + "backup_settings": "備份è¨ç½®åˆ°æ–‡ä»¶", + "backup_restore": "è¨å®šå‚™ä»½" + }, + "sensitive_by_default": "默èªæ¨™è¨˜ç™¼æ–‡ç‚ºæ•æ„Ÿå…§å®¹", + "right_sidebar": "在å³å´é¡¯ç¤ºå´é‚Šæ¬„", + "hide_shoutbox": "éš±è—實例留言框" }, "chats": { "more": "更多", @@ -572,16 +606,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} 天", @@ -639,7 +677,8 @@ "attachments_sensitive": "標記附件為æ•æ„Ÿå…§å®¹", "account_not_locked_warning_link": "上鎖", "default": "剛剛抵é”æ´›æ‰ç£¯ã€‚", - "empty_status_error": "無法發佈沒有附件的空發文" + "empty_status_error": "ä¸èƒ½ç™¼å¸ƒæ²’有內容,沒有附件的發文", + "post": "發é€" }, "errors": { "storage_unavailable": "Pleroma無法訪å•ç€è¦½å™¨å˜å„²ã€‚您的登錄å或本地è¨ç½®å°‡ä¸æœƒä¿å˜ï¼Œæ‚¨å¯èƒ½æœƒé‡åˆ°æ„外å•é¡Œã€‚嘗試啟用Cookie。" @@ -655,13 +694,16 @@ "reload": "é‡æ–°è¼‰å…¥", "up_to_date": "已是最新", "no_more_statuses": "没有更多發文", - "no_statuses": "没有發文" + "no_statuses": "没有發文", + "error": "å–得時間線時發生錯誤:{0}", + "socket_reconnected": "已建立實時連接", + "socket_broke": "丟失實時連接:CloseEvent代碼{0}" }, "interactions": { "load_older": "載入更早的互動", "moves": "用戶é·ç§»", "follows": "新的關注者", - "favs_repeats": "轉發和收è—" + "favs_repeats": "轉發和喜æ¡" }, "selectable_list": { "select_all": "é¸æ“‡å…¨éƒ¨" @@ -690,7 +732,10 @@ "registration": "註冊", "password_confirm": "確èªå¯†ç¢¼", "email": "é›»å郵箱", - "bio": "簡介" + "bio": "簡介", + "reason_placeholder": "æ¤å¯¦ä¾‹çš„註冊需è¦æ‰‹å‹•æ‰¹å‡†ã€‚\n請讓管ç†çŸ¥é“您為什麼想è¦è¨»å†Šã€‚", + "reason": "註冊ç†ç”±", + "register": "註冊" }, "user_card": { "its_you": "å°±æ˜¯ä½ ï¼!", @@ -726,7 +771,6 @@ "follow": "關注", "follow_sent": "請求已發é€ï¼", "follow_progress": "請求ä¸â€¦", - "follow_again": "å†æ¬¡ç™¼é€è«‹æ±‚?", "follow_unfollow": "å–消關注", "followees": "æ£åœ¨é—œæ³¨", "followers": "關注者", @@ -746,7 +790,19 @@ "unmute": "å–消éœéŸ³", "unmute_progress": "å–消éœéŸ³ä¸â€¦", "hide_repeats": "éš±è—轉發", - "show_repeats": "顯示轉發" + "show_repeats": "顯示轉發", + "roles": { + "moderator": "主æŒäºº", + "admin": "管ç†å“¡" + }, + "highlight": { + "disabled": "ç„¡çªå‡ºé¡¯ç¤º", + "solid": "單色背景", + "striped": "æ¢ç´‹èƒŒæ™¯", + "side": "彩æ¢" + }, + "bot": "機器人", + "edit_profile": "編輯個人資料" }, "user_profile": { "timeline_title": "用戶時間線", @@ -788,7 +844,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 index 90ee2887833cd82627732c5aaf3d2097329908f8..3895da89b8cea1462ea2ed77d341ffcf0dd28a55 100644 --- a/src/main.js +++ b/src/main.js @@ -11,7 +11,7 @@ import statusesModule from './modules/statuses.js' import usersModule from './modules/users.js' import apiModule from './modules/api.js' import configModule from './modules/config.js' -import chatModule from './modules/chat.js' +import shoutModule from './modules/shout.js' import oauthModule from './modules/oauth.js' import authFlowModule from './modules/auth_flow.js' import mediaViewerModule from './modules/media_viewer.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) @@ -90,7 +88,7 @@ const persistedStateOptions = { users: usersModule, api: apiModule, config: configModule, - chat: chatModule, + shout: shoutModule, oauth: oauthModule, authFlow: authFlowModule, mediaViewer: mediaViewerModule, diff --git a/src/modules/api.js b/src/modules/api.js index 08485a30a5994984c1fe7d7fdc011168722026c4..54f943564fe4c90414b61b647f9833ab4e5b0ac9 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -3,8 +3,11 @@ import { WSConnectionStatus } from '../services/api/api.service.js' import { maybeShowChatNotification } from '../services/chat_utils/chat_utils.js' import { Socket } from 'phoenix' +const retryTimeout = (multiplier) => 1000 * multiplier + const api = { state: { + retryMultiplier: 1, backendInteractor: backendInteractorService(), fetchers: {}, socket: null, @@ -34,18 +37,43 @@ const api = { }, setMastoUserSocketStatus (state, value) { state.mastoUserSocketStatus = value + }, + incrementRetryMultiplier (state) { + state.retryMultiplier = Math.max(++state.retryMultiplier, 3) + }, + resetRetryMultiplier (state) { + state.retryMultiplier = 1 } }, actions: { - // Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets - enableMastoSockets (store) { - const { state, dispatch } = store - if (state.mastoUserSocket) return + /** + * Global MastoAPI socket control, in future should disable ALL sockets/(re)start relevant sockets + * + * @param {Boolean} [initial] - whether this enabling happened at boot time or not + */ + enableMastoSockets (store, initial) { + const { state, dispatch, commit } = store + // Do not initialize unless nonexistent or closed + if ( + state.mastoUserSocket && + ![ + WebSocket.CLOSED, + WebSocket.CLOSING + ].includes(state.mastoUserSocket.getState()) + ) { + return + } + if (initial) { + commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING_INITIAL) + } else { + commit('setMastoUserSocketStatus', WSConnectionStatus.STARTING) + } return dispatch('startMastoUserSocket') }, disableMastoSockets (store) { - const { state, dispatch } = store + const { state, dispatch, commit } = store if (!state.mastoUserSocket) return + commit('setMastoUserSocketStatus', WSConnectionStatus.DISABLED) return dispatch('stopMastoUserSocket') }, @@ -91,11 +119,29 @@ const api = { } ) state.mastoUserSocket.addEventListener('open', () => { + // Do not show notification when we just opened up the page + if (state.mastoUserSocketStatus !== WSConnectionStatus.STARTING_INITIAL) { + dispatch('pushGlobalNotice', { + level: 'success', + messageKey: 'timeline.socket_reconnected', + timeout: 5000 + }) + } + // Stop polling if we were errored or disabled + if (new Set([ + WSConnectionStatus.ERROR, + WSConnectionStatus.DISABLED + ]).has(state.mastoUserSocketStatus)) { + dispatch('stopFetchingTimeline', { timeline: 'friends' }) + dispatch('stopFetchingNotifications') + dispatch('stopFetchingChats') + } + commit('resetRetryMultiplier') commit('setMastoUserSocketStatus', WSConnectionStatus.JOINED) }) state.mastoUserSocket.addEventListener('error', ({ detail: error }) => { console.error('Error in MastoAPI websocket:', error) - commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR) + // TODO is this needed? dispatch('clearOpenedChats') }) state.mastoUserSocket.addEventListener('close', ({ detail: closeEvent }) => { @@ -106,14 +152,26 @@ const api = { const { code } = closeEvent if (ignoreCodes.has(code)) { console.debug(`Not restarting socket becasue of closure code ${code} is in ignore list`) + commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED) } else { console.warn(`MastoAPI websocket disconnected, restarting. CloseEvent code: ${code}`) - dispatch('startFetchingTimeline', { timeline: 'friends' }) - dispatch('startFetchingNotifications') - dispatch('startFetchingChats') - dispatch('restartMastoUserSocket') + setTimeout(() => { + dispatch('startMastoUserSocket') + }, retryTimeout(state.retryMultiplier)) + commit('incrementRetryMultiplier') + if (state.mastoUserSocketStatus !== WSConnectionStatus.ERROR) { + dispatch('startFetchingTimeline', { timeline: 'friends' }) + dispatch('startFetchingNotifications') + dispatch('startFetchingChats') + dispatch('pushGlobalNotice', { + level: 'error', + messageKey: 'timeline.socket_broke', + messageArgs: [code], + timeout: 5000 + }) + } + commit('setMastoUserSocketStatus', WSConnectionStatus.ERROR) } - commit('setMastoUserSocketStatus', WSConnectionStatus.CLOSED) dispatch('clearOpenedChats') }) resolve() @@ -122,15 +180,6 @@ const api = { } }) }, - restartMastoUserSocket ({ dispatch }) { - // This basically starts MastoAPI user socket and stops conventional - // fetchers when connection reestablished - return dispatch('startMastoUserSocket').then(() => { - dispatch('stopFetchingTimeline', { timeline: 'friends' }) - dispatch('stopFetchingNotifications') - dispatch('stopFetchingChats') - }) - }, stopMastoUserSocket ({ state, dispatch }) { dispatch('startFetchingTimeline', { timeline: 'friends' }) dispatch('startFetchingNotifications') @@ -156,6 +205,13 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: timeline, fetcher }) }, + fetchTimeline (store, timeline, { ...rest }) { + store.state.backendInteractor.fetchTimeline({ + store, + timeline, + ...rest + }) + }, // Notifications startFetchingNotifications (store) { @@ -168,6 +224,12 @@ const api = { if (!fetcher) return store.commit('removeFetcher', { fetcherName: 'notifications', fetcher }) }, + fetchNotifications (store, { ...rest }) { + store.state.backendInteractor.fetchNotifications({ + store, + ...rest + }) + }, // Follow requests startFetchingFollowRequests (store) { @@ -193,12 +255,12 @@ const api = { initializeSocket ({ dispatch, commit, state, rootState }) { // Set up websocket connection const token = state.wsToken - if (rootState.instance.chatAvailable && typeof token !== 'undefined' && state.socket === null) { + if (rootState.instance.shoutAvailable && typeof token !== 'undefined' && state.socket === null) { const socket = new Socket('/socket', { params: { token } }) socket.connect() commit('setSocket', socket) - dispatch('initializeChat', socket) + dispatch('initializeShout', socket) } }, disconnectFromSocket ({ commit, state }) { diff --git a/src/modules/chats.js b/src/modules/chats.js index 0a373d88a892feb811fad3f729502eaa799f2893..69d683bd85637cc8d40812c759b88c90b3be6f53 100644 --- 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 index e591a50600fa42a1919b87dc23731a7b24e1b338..825535cd70c7964de617579e9091fc5b3076009b 100644 --- a/src/modules/config.js +++ b/src/modules/config.js @@ -11,7 +11,8 @@ const browserLocale = (window.navigator.language || 'en').split('-')[0] */ export const multiChoiceProperties = [ 'postContentType', - 'subjectLineBehavior' + 'subjectLineBehavior', + 'mentionLinkDisplay' // short | full_for_remote | full ] export const defaultState = { @@ -21,8 +22,11 @@ export const defaultState = { customThemeSource: undefined, hideISP: false, hideInstanceWallpaper: false, + hideShoutbox: false, // bad name: actually hides posts of muted USERS hideMutedPosts: undefined, // instance default + hideMutedThreads: undefined, // instance default + hideWordFilteredPosts: undefined, // instance default collapseMessageWithSubject: undefined, // instance default padEmoji: true, hideAttachments: false, @@ -34,6 +38,7 @@ export const defaultState = { loopVideoSilentOnly: true, streaming: false, emojiReactionsOnTimeline: true, + alwaysShowNewPostButton: false, autohideFloatingPostButton: false, pauseOnUnfocused: true, stopGifs: false, @@ -55,6 +60,7 @@ export const defaultState = { interfaceLanguage: browserLocale, hideScopeNotice: false, useStreamingApi: false, + sidebarRight: undefined, // instance default scopeCopy: undefined, // instance default subjectLineBehavior: undefined, // instance default alwaysShowSubjectInput: undefined, // instance default @@ -66,9 +72,17 @@ export const defaultState = { useOneClickNsfw: false, useContainFit: false, greentext: undefined, // instance default + useAtIcon: undefined, // instance default + mentionLinkDisplay: undefined, // instance default + mentionLinkShowTooltip: undefined, // instance default + mentionLinkShowAvatar: undefined, // instance default + mentionLinkFadeDomain: undefined, // instance default + mentionLinkShowYous: undefined, // instance default + mentionLinkBoldenYou: 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 @@ -77,18 +91,23 @@ 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, + // Do not override with undefined + ...Object.fromEntries(Object.entries(state).filter(([k, v]) => v !== undefined)) } } }, @@ -106,6 +125,20 @@ const config = { } }, actions: { + loadSettings ({ dispatch }, data) { + const knownKeys = new Set(Object.keys(defaultState)) + const presentKeys = new Set(Object.keys(data)) + const intersection = new Set() + for (let elem of presentKeys) { + if (knownKeys.has(elem)) { + intersection.add(elem) + } + } + + intersection.forEach( + name => dispatch('setOption', { name, value: data[name] }) + ) + }, setHighlight ({ commit, dispatch }, { user, color, type }) { commit('setHighlight', { user, color, type }) }, diff --git a/src/modules/instance.js b/src/modules/instance.js index 411b1caa87bfb2d756f13af4a67d12787e2bf188..1abd784f0662db35a03ec4bfde1b27022c6a9ec5 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -19,10 +19,19 @@ const defaultState = { defaultBanner: '/images/banner.png', background: '/static/aurora_borealis.jpg', collapseMessageWithSubject: false, - disableChat: false, greentext: false, + useAtIcon: false, + mentionLinkDisplay: 'short', + mentionLinkShowTooltip: true, + mentionLinkShowAvatar: false, + mentionLinkFadeDomain: true, + mentionLinkShowYous: false, + mentionLinkBoldenYou: true, hideFilteredStatuses: false, + // bad name: actually hides posts of muted USERS hideMutedPosts: false, + hideMutedThreads: true, + hideWordFilteredPosts: false, hidePostStats: false, hideSitename: false, hideUserStats: false, @@ -43,6 +52,7 @@ const defaultState = { subjectLineBehavior: 'email', theme: 'pleroma-dark', virtualScrolling: true, + sensitiveByDefault: false, // Nasty stuff customEmoji: [], @@ -56,7 +66,7 @@ const defaultState = { knownDomains: [], // Feature-set, apparently, not everything here is reported... - chatAvailable: false, + shoutAvailable: false, pleromaChatMessagesAvailable: false, gopherAvailable: false, mediaProxyAvailable: false, @@ -97,6 +107,9 @@ const instance = { return instanceDefaultProperties .map(key => [key, state[key]]) .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}) + }, + instanceDomain (state) { + return new URL(state.server).hostname } }, actions: { @@ -106,7 +119,7 @@ const instance = { case 'name': dispatch('setPageTitle') break - case 'chatAvailable': + case 'shoutAvailable': if (value) { dispatch('initializeSocket') } diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js index 721c25e6fb1d9302a5366b71105c2fdb45c32c83..ebcba01d4cf697a6d831b51a254b604f00fbe030 100644 --- a/src/modules/media_viewer.js +++ b/src/modules/media_viewer.js @@ -1,4 +1,5 @@ import fileTypeService from '../services/file_type/file_type.service.js' +const supportedTypes = new Set(['image', 'video', 'audio', 'flash']) const mediaViewer = { state: { @@ -10,7 +11,7 @@ const mediaViewer = { setMedia (state, media) { state.media = media }, - setCurrent (state, index) { + setCurrentMedia (state, index) { state.activated = true state.currentIndex = index }, @@ -22,13 +23,13 @@ const mediaViewer = { setMedia ({ commit }, attachments) { const media = attachments.filter(attachment => { const type = fileTypeService.fileType(attachment.mimetype) - return type === 'image' || type === 'video' || type === 'audio' + return supportedTypes.has(type) }) commit('setMedia', media) }, - setCurrent ({ commit, state }, current) { + setCurrentMedia ({ commit, state }, current) { const index = state.media.indexOf(current) - commit('setCurrent', index || 0) + commit('setCurrentMedia', index || 0) }, closeMediaViewer ({ commit }) { commit('close') diff --git a/src/modules/chat.js b/src/modules/shout.js similarity index 90% rename from src/modules/chat.js rename to src/modules/shout.js index c798549dd1ee940329dd7194fa9905a44c3971c5..507a4d835ac1641eea0e91c122aff908c8ad96c6 100644 --- a/src/modules/chat.js +++ b/src/modules/shout.js @@ -1,4 +1,4 @@ -const chat = { +const shout = { state: { messages: [], channel: { state: '' } @@ -16,7 +16,7 @@ const chat = { } }, actions: { - initializeChat (store, socket) { + initializeShout (store, socket) { const channel = socket.channel('chat:public') channel.on('new_msg', (msg) => { store.commit('addMessage', msg) @@ -30,4 +30,4 @@ const chat = { } } -export default chat +export default shout diff --git a/src/modules/statuses.js b/src/modules/statuses.js index d5343cd4ef269584ef3aacd161c3f086f53c9a13..cb2a8d4617bb06a9929a1244caa0679b25b96444 100644 --- 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 @@ -327,12 +347,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/modules/users.js b/src/modules/users.js index 655db4c7ed08e6c35e92cd8bd375f838a474b644..05ff44d5d9880c920d9ba90d9c1322da248a2319 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -246,6 +246,11 @@ export const getters = { } return result }, + findUserByUrl: state => query => { + return state.users + .find(u => u.statusnet_profile_url && + u.statusnet_profile_url.toLowerCase() === query.toLowerCase()) + }, relationship: state => id => { const rel = id && state.relationships[id] return rel || { id, loading: true } @@ -388,7 +393,7 @@ const users = { toggleActivationStatus ({ rootState, commit }, { user }) { const api = user.deactivated ? rootState.api.backendInteractor.activateUser : rootState.api.backendInteractor.deactivateUser api({ user }) - .then(({ deactivated }) => commit('updateActivationStatus', { user, deactivated })) + .then((user) => { let deactivated = !user.is_active; commit('updateActivationStatus', { user, deactivated }) }) }, registerPushNotifications (store) { const token = store.state.currentUser.credentials @@ -531,7 +536,7 @@ const users = { if (user.token) { store.dispatch('setWsToken', user.token) - // Initialize the chat socket. + // Initialize the shout socket. store.dispatch('initializeSocket') } @@ -547,9 +552,10 @@ const users = { } if (store.getters.mergedConfig.useStreamingApi) { - store.dispatch('enableMastoSockets').catch((error) => { + store.dispatch('fetchTimeline', 'friends', { since: null }) + store.dispatch('fetchNotifications', { since: null }) + store.dispatch('enableMastoSockets', true).catch((error) => { console.error('Failed initializing MastoAPI Streaming socket', error) - startPolling() }).then(() => { store.dispatch('fetchChats', { latest: true }) setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 27ea5199b185cdcca71ab3da6134d46fa36f2c16..c17d04764c306faf8b0c86d9128317d94e45849b 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -1159,6 +1159,7 @@ export const ProcessedWS = ({ // 1000 = Normal Closure eventTarget.close = () => { socket.close(1000, 'Shutting down socket') } + eventTarget.getState = () => socket.readyState return eventTarget } @@ -1190,7 +1191,10 @@ export const handleMastoWS = (wsEvent) => { export const WSConnectionStatus = Object.freeze({ 'JOINED': 1, 'CLOSED': 2, - 'ERROR': 3 + 'ERROR': 3, + 'DISABLED': 4, + 'STARTING': 5, + 'STARTING_INITIAL': 6 }) const chats = ({ credentials }) => { diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js index 45e6bd0e1735868976c6f59fba540d2679053246..4a40f5b5fab961317d5940892304211f06c85c2e 100644 --- a/src/services/backend_interactor_service/backend_interactor_service.js +++ b/src/services/backend_interactor_service/backend_interactor_service.js @@ -1,17 +1,25 @@ import apiService, { getMastodonSocketURI, ProcessedWS } from '../api/api.service.js' -import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js' +import timelineFetcher from '../timeline_fetcher/timeline_fetcher.service.js' import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js' import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service' const backendInteractorService = credentials => ({ startFetchingTimeline ({ timeline, store, userId = false, tag }) { - return timelineFetcherService.startFetching({ timeline, store, credentials, userId, tag }) + return timelineFetcher.startFetching({ timeline, store, credentials, userId, tag }) + }, + + fetchTimeline (args) { + return timelineFetcher.fetchAndUpdate({ ...args, credentials }) }, startFetchingNotifications ({ store }) { return notificationsFetcher.startFetching({ store, credentials }) }, + fetchNotifications (args) { + return notificationsFetcher.fetchAndUpdate({ ...args, credentials }) + }, + startFetchingFollowRequests ({ store }) { return followRequestFetcher.startFetching({ store, credentials }) }, diff --git a/src/services/chat_service/chat_service.js b/src/services/chat_service/chat_service.js index e653ebc12092e5a0bcd4e98a8a4e862f44cf6810..92ff689d17f45872289bf59d093728956efda179 100644 --- 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 index 00021ce0c935727545551639c27a772f7ee265a8..bf980669aef7b99ab9081f41fa7ebc0d689ce30a 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -54,17 +54,20 @@ export const parseUser = (data) => { return output } - output.name = data.display_name - output.name_html = addEmojis(escape(data.display_name), data.emojis) + output.emoji = data.emojis + output.name = escape(data.display_name) + output.name_html = output.name + output.name_unescaped = data.display_name output.description = data.note - output.description_html = addEmojis(data.note, data.emojis) + // TODO cleanup this shit, output.description is overriden with source data + output.description_html = data.note output.fields = data.fields output.fields_html = data.fields.map(field => { return { - name: addEmojis(escape(field.name), data.emojis), - value: addEmojis(field.value, data.emojis) + name: escape(field.name), + value: field.value } }) output.fields_text = data.fields.map(field => { @@ -203,15 +206,16 @@ export const parseUser = (data) => { output.rights = output.rights || {} output.notification_settings = output.notification_settings || {} - // Convert punycode to unicode - if (output.screen_name.includes('@')) { + // Convert punycode to unicode for UI + output.screen_name_ui = output.screen_name + if (output.screen_name && output.screen_name.includes('@')) { const parts = output.screen_name.split('@') let unicodeDomain = punycode.toUnicode(parts[1]) if (unicodeDomain !== parts[1]) { // 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('@') } } @@ -238,16 +242,6 @@ export const parseAttachment = (data) => { return output } -export const addEmojis = (string, emojis) => { - const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g - return emojis.reduce((acc, emoji) => { - const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&') - return acc.replace( - new RegExp(`:${regexSafeShortCode}:`, 'g'), - `<img src='${emoji.url}' alt=':${emoji.shortcode}:' title=':${emoji.shortcode}:' class='emoji' />` - ) - }, string) -} export const parseStatus = (data) => { const output = {} @@ -265,7 +259,8 @@ export const parseStatus = (data) => { output.type = data.reblog ? 'retweet' : 'status' output.nsfw = data.sensitive - output.statusnet_html = addEmojis(data.content, data.emojis) + output.raw_html = data.content + output.emojis = data.emojis output.tags = data.tags @@ -292,13 +287,13 @@ export const parseStatus = (data) => { output.retweeted_status = parseStatus(data.reblog) } - output.summary_html = addEmojis(escape(data.spoiler_text), data.emojis) + output.summary_raw_html = escape(data.spoiler_text) output.external_url = data.url output.poll = data.poll if (output.poll) { output.poll.options = (output.poll.options || []).map(field => ({ ...field, - title_html: addEmojis(escape(field.title), data.emojis) + title_html: escape(field.title) })) } output.pinned = data.pinned @@ -324,7 +319,7 @@ export const parseStatus = (data) => { output.nsfw = data.nsfw } - output.statusnet_html = data.statusnet_html + output.raw_html = data.statusnet_html output.text = data.text output.in_reply_to_status_id = data.in_reply_to_status_id @@ -450,11 +445,8 @@ export const parseChatMessage = (message) => { output.id = message.id output.created_at = new Date(message.created_at) output.chat_id = message.chat_id - if (message.content) { - output.content = addEmojis(message.content, message.emojis) - } else { - output.content = '' - } + output.emojis = message.emojis + output.content = message.content if (message.attachment) { output.attachments = [parseAttachment(message.attachment)] } else { diff --git a/src/services/export_import/export_import.js b/src/services/export_import/export_import.js new file mode 100644 index 0000000000000000000000000000000000000000..ac67cf9c528387112f10bc00df3972901d898643 --- /dev/null +++ b/src/services/export_import/export_import.js @@ -0,0 +1,55 @@ +export const newExporter = ({ + filename = 'data', + getExportedObject +}) => ({ + exportData () { + const stringified = JSON.stringify(getExportedObject(), null, 2) // Pretty-print and indent with 2 spaces + + // Create an invisible link with a data url and simulate a click + const e = document.createElement('a') + e.setAttribute('download', `${filename}.json`) + e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified)) + e.style.display = 'none' + + document.body.appendChild(e) + e.click() + document.body.removeChild(e) + } +}) + +export const newImporter = ({ + onImport, + onImportFailure, + validator = () => true +}) => ({ + importData () { + const filePicker = document.createElement('input') + filePicker.setAttribute('type', 'file') + filePicker.setAttribute('accept', '.json') + + filePicker.addEventListener('change', event => { + if (event.target.files[0]) { + // eslint-disable-next-line no-undef + const reader = new FileReader() + reader.onload = ({ target }) => { + try { + const parsed = JSON.parse(target.result) + const validationResult = validator(parsed) + if (validationResult === true) { + onImport(parsed) + } else { + onImportFailure({ validationResult }) + } + } catch (error) { + onImportFailure({ error }) + } + } + reader.readAsText(event.target.files[0]) + } + }) + + document.body.appendChild(filePicker) + filePicker.click() + document.body.removeChild(filePicker) + } +}) diff --git a/src/services/favicon_service/favicon_service.js b/src/services/favicon_service/favicon_service.js index d1ddee41026e948e5fefa1545a0ddfbd2f2428c2..7e19629d98ce043568fc8c331efbbc03d39ccdfa 100644 --- a/src/services/favicon_service/favicon_service.js +++ b/src/services/favicon_service/favicon_service.js @@ -1,52 +1,58 @@ -import { find } from 'lodash' - const createFaviconService = () => { - let favimg, favcanvas, favcontext, favicon + const favicons = [] const faviconWidth = 128 const faviconHeight = 128 const badgeRadius = 32 const initFaviconService = () => { - const nodes = document.getElementsByTagName('link') - favicon = find(nodes, node => node.rel === 'icon') - if (favicon) { - favcanvas = document.createElement('canvas') - favcanvas.width = faviconWidth - favcanvas.height = faviconHeight - favimg = new Image() - favimg.src = favicon.href - favcontext = favcanvas.getContext('2d') - } + const nodes = document.querySelectorAll('link[rel="icon"]') + nodes.forEach(favicon => { + if (favicon) { + const favcanvas = document.createElement('canvas') + favcanvas.width = faviconWidth + favcanvas.height = faviconHeight + const favimg = new Image() + favimg.crossOrigin = 'anonymous' + favimg.src = favicon.href + const favcontext = favcanvas.getContext('2d') + favicons.push({ favcanvas, favimg, favcontext, favicon }) + } + }) } const isImageLoaded = (img) => img.complete && img.naturalHeight !== 0 const clearFaviconBadge = () => { - if (!favimg || !favcontext || !favicon) return + if (favicons.length === 0) return + favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => { + if (!favimg || !favcontext || !favicon) return - favcontext.clearRect(0, 0, faviconWidth, faviconHeight) - if (isImageLoaded(favimg)) { - favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) - } - favicon.href = favcanvas.toDataURL('image/png') + favcontext.clearRect(0, 0, faviconWidth, faviconHeight) + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) + } + favicon.href = favcanvas.toDataURL('image/png') + }) } const drawFaviconBadge = () => { - if (!favimg || !favcontext || !favcontext) return - + if (favicons.length === 0) return clearFaviconBadge() + favicons.forEach(({ favimg, favcanvas, favcontext, favicon }) => { + if (!favimg || !favcontext || !favcontext) return + + const style = getComputedStyle(document.body) + const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}` - const style = getComputedStyle(document.body) - const badgeColor = `${style.getPropertyValue('--badgeNotification') || 'rgb(240, 100, 100)'}` - - if (isImageLoaded(favimg)) { - favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) - } - favcontext.fillStyle = badgeColor - favcontext.beginPath() - favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false) - favcontext.fill() - favicon.href = favcanvas.toDataURL('image/png') + if (isImageLoaded(favimg)) { + favcontext.drawImage(favimg, 0, 0, favimg.width, favimg.height, 0, 0, faviconWidth, faviconHeight) + } + favcontext.fillStyle = badgeColor + favcontext.beginPath() + favcontext.arc(faviconWidth - badgeRadius, badgeRadius, badgeRadius, 0, 2 * Math.PI, false) + favcontext.fill() + favicon.href = favcanvas.toDataURL('image/png') + }) } return { diff --git a/src/services/file_type/file_type.service.js b/src/services/file_type/file_type.service.js index 2a046bec7debf70c03cd6d6f4b173fb33e97e9c4..5182ecd18b87c7c8b7ae18ce099c66bde82f27ea 100644 --- a/src/services/file_type/file_type.service.js +++ b/src/services/file_type/file_type.service.js @@ -2,6 +2,10 @@ // or the entire service could be just mimetype service that only operates // on mimetypes and not files. Currently the naming is confusing. const fileType = mimetype => { + if (mimetype.match(/flash/)) { + return 'flash' + } + if (mimetype.match(/text\/html/)) { return 'html' } diff --git a/src/services/html_converter/html_line_converter.service.js b/src/services/html_converter/html_line_converter.service.js new file mode 100644 index 0000000000000000000000000000000000000000..5eeaa7cb205e7bb666733e66da6368274e0673dc --- /dev/null +++ b/src/services/html_converter/html_line_converter.service.js @@ -0,0 +1,136 @@ +import { getTagName } from './utility.service.js' + +/** + * This is a tiny purpose-built HTML parser/processor. This basically detects + * any type of visual newline and converts entire HTML into a array structure. + * + * Text nodes are represented as object with single property - text - containing + * the visual line. Intended usage is to process the array with .map() in which + * map function returns a string and resulting array can be converted back to html + * with a .join(''). + * + * Generally this isn't very useful except for when you really need to either + * modify visual lines (greentext i.e. simple quoting) or do something with + * first/last line. + * + * known issue: doesn't handle CDATA so nested CDATA might not work well + * + * @param {Object} input - input data + * @return {(string|{ text: string })[]} processed html in form of a list. + */ +export const convertHtmlToLines = (html = '') => { + // Elements that are implicitly self-closing + // https://developer.mozilla.org/en-US/docs/Glossary/empty_element + const emptyElements = new Set([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', + 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' + ]) + // Block-level element (they make a visual line) + // https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements + const blockElements = new Set([ + 'address', 'article', 'aside', 'blockquote', 'details', 'dialog', 'dd', + 'div', 'dl', 'dt', 'fieldset', 'figcaption', 'figure', 'footer', 'form', + 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'li', 'main', + 'nav', 'ol', 'p', 'pre', 'section', 'table', 'ul' + ]) + // br is very weird in a way that it's technically not block-level, it's + // essentially converted to a \n (or \r\n). There's also wbr but it doesn't + // guarantee linebreak, only suggest it. + const linebreakElements = new Set(['br']) + + const visualLineElements = new Set([ + ...blockElements.values(), + ...linebreakElements.values() + ]) + + // All block-level elements that aren't empty elements, i.e. not <hr> + const nonEmptyElements = new Set(visualLineElements) + // Difference + for (let elem of emptyElements) { + nonEmptyElements.delete(elem) + } + + // All elements that we are recognizing + const allElements = new Set([ + ...nonEmptyElements.values(), + ...emptyElements.values() + ]) + + let buffer = [] // Current output buffer + const level = [] // How deep we are in tags and which tags were there + let textBuffer = '' // Current line content + let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag + + const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer + if (textBuffer.trim().length > 0) { + buffer.push({ level: [...level], text: textBuffer }) + } else { + buffer.push(textBuffer) + } + textBuffer = '' + } + + const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing + flush() + buffer.push(tag) + } + + const handleOpen = (tag) => { // handles opening tags + flush() + buffer.push(tag) + level.unshift(getTagName(tag)) + } + + const handleClose = (tag) => { // handles closing tags + if (level[0] === getTagName(tag)) { + flush() + buffer.push(tag) + level.shift() + } else { // Broken case + textBuffer += tag + } + } + + for (let i = 0; i < html.length; i++) { + const char = html[i] + if (char === '<' && tagBuffer === null) { + tagBuffer = char + } else if (char !== '>' && tagBuffer !== null) { + tagBuffer += char + } else if (char === '>' && tagBuffer !== null) { + tagBuffer += char + const tagFull = tagBuffer + tagBuffer = null + const tagName = getTagName(tagFull) + if (allElements.has(tagName)) { + if (linebreakElements.has(tagName)) { + handleBr(tagFull) + } else if (nonEmptyElements.has(tagName)) { + if (tagFull[1] === '/') { + handleClose(tagFull) + } else if (tagFull[tagFull.length - 2] === '/') { + // self-closing + handleBr(tagFull) + } else { + handleOpen(tagFull) + } + } else { + textBuffer += tagFull + } + } else { + textBuffer += tagFull + } + } else if (char === '\n') { + handleBr(char) + } else { + textBuffer += char + } + } + if (tagBuffer) { + textBuffer += tagBuffer + } + + flush() + + return buffer +} diff --git a/src/services/html_converter/html_tree_converter.service.js b/src/services/html_converter/html_tree_converter.service.js new file mode 100644 index 0000000000000000000000000000000000000000..247a817366dd34e087e048a4067188db18136940 --- /dev/null +++ b/src/services/html_converter/html_tree_converter.service.js @@ -0,0 +1,98 @@ +import { getTagName } from './utility.service.js' +import { unescape } from 'lodash' + +/** + * This is a not-so-tiny purpose-built HTML parser/processor. This parses html + * and converts it into a tree structure representing tag openers/closers and + * children. + * + * Structure follows this pattern: [opener, [...children], closer] except root + * node which is just [...children]. Text nodes can only be within children and + * are represented as strings. + * + * Intended use is to convert HTML structure and then recursively iterate over it + * most likely using a map. Very useful for dynamically rendering html replacing + * tags with JSX elements in a render function. + * + * known issue: doesn't handle CDATA so CDATA might not work well + * known issue: doesn't handle HTML comments + * + * @param {Object} input - input data + * @return {string} processed html + */ +export const convertHtmlToTree = (html = '') => { + // Elements that are implicitly self-closing + // https://developer.mozilla.org/en-US/docs/Glossary/empty_element + const emptyElements = new Set([ + 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', + 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' + ]) + // TODO For future - also parse HTML5 multi-source components? + + const buffer = [] // Current output buffer + const levels = [['', buffer]] // How deep we are in tags and which tags were there + let textBuffer = '' // Current line content + let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag + + const getCurrentBuffer = () => { + return levels[levels.length - 1][1] + } + + const flushText = () => { // Processes current line buffer, adds it to output buffer and clears line buffer + if (textBuffer === '') return + getCurrentBuffer().push(textBuffer) + textBuffer = '' + } + + const handleSelfClosing = (tag) => { + getCurrentBuffer().push([tag]) + } + + const handleOpen = (tag) => { + const curBuf = getCurrentBuffer() + const newLevel = [unescape(tag), []] + levels.push(newLevel) + curBuf.push(newLevel) + } + + const handleClose = (tag) => { + const currentTag = levels[levels.length - 1] + if (getTagName(levels[levels.length - 1][0]) === getTagName(tag)) { + currentTag.push(tag) + levels.pop() + } else { + getCurrentBuffer().push(tag) + } + } + + for (let i = 0; i < html.length; i++) { + const char = html[i] + if (char === '<' && tagBuffer === null) { + flushText() + tagBuffer = char + } else if (char !== '>' && tagBuffer !== null) { + tagBuffer += char + } else if (char === '>' && tagBuffer !== null) { + tagBuffer += char + const tagFull = tagBuffer + tagBuffer = null + const tagName = getTagName(tagFull) + if (tagFull[1] === '/') { + handleClose(tagFull) + } else if (emptyElements.has(tagName) || tagFull[tagFull.length - 2] === '/') { + // self-closing + handleSelfClosing(tagFull) + } else { + handleOpen(tagFull) + } + } else { + textBuffer += char + } + } + if (tagBuffer) { + textBuffer += tagBuffer + } + + flushText() + return buffer +} diff --git a/src/services/html_converter/utility.service.js b/src/services/html_converter/utility.service.js new file mode 100644 index 0000000000000000000000000000000000000000..4d0c36c2bf779e867bc49eec25f799b76ba099f3 --- /dev/null +++ b/src/services/html_converter/utility.service.js @@ -0,0 +1,73 @@ +/** + * Extract tag name from tag opener/closer. + * + * @param {String} tag - tag string, i.e. '<a href="...">' + * @return {String} - tagname, i.e. "div" + */ +export const getTagName = (tag) => { + const result = /(?:<\/(\w+)>|<(\w+)\s?.*?\/?>)/gi.exec(tag) + return result && (result[1] || result[2]) +} + +/** + * Extract attributes from tag opener. + * + * @param {String} tag - tag string, i.e. '<a href="...">' + * @return {Object} - map of attributes key = attribute name, value = attribute value + * attributes without values represented as boolean true + */ +export const getAttrs = tag => { + const innertag = tag + .substring(1, tag.length - 1) + .replace(new RegExp('^' + getTagName(tag)), '') + .replace(/\/?$/, '') + .trim() + const attrs = Array.from(innertag.matchAll(/([a-z0-9-]+)(?:=("[^"]+?"|'[^']+?'))?/gi)) + .map(([trash, key, value]) => [key, value]) + .map(([k, v]) => { + if (!v) return [k, true] + return [k, v.substring(1, v.length - 1)] + }) + return Object.fromEntries(attrs) +} + +/** + * Finds shortcodes in text + * + * @param {String} text - original text to find emojis in + * @param {{ url: String, shortcode: Sring }[]} emoji - list of shortcodes to find + * @param {Function} processor - function to call on each encountered emoji, + * function is passed single object containing matching emoji ({ url, shortcode }) + * return value will be inserted into resulting array instead of :shortcode: + * @return {Array} resulting array with non-emoji parts of text and whatever {processor} + * returned for emoji + */ +export const processTextForEmoji = (text, emojis, processor) => { + const buffer = [] + let textBuffer = '' + for (let i = 0; i < text.length; i++) { + const char = text[i] + if (char === ':') { + const next = text.slice(i + 1) + let found = false + for (let emoji of emojis) { + if (next.slice(0, emoji.shortcode.length + 1) === (emoji.shortcode + ':')) { + found = emoji + break + } + } + if (found) { + buffer.push(textBuffer) + textBuffer = '' + buffer.push(processor(found)) + i += found.shortcode.length + 1 + } else { + textBuffer += char + } + } else { + textBuffer += char + } + } + if (textBuffer) buffer.push(textBuffer) + return buffer +} diff --git a/src/services/notification_utils/notification_utils.js b/src/services/notification_utils/notification_utils.js index b338eb8b3b5fe3a382f964defb59a500f55d5bde..8c70ea535c9cd53e7f5943ed49b6ec5777dff499 100644 --- a/src/services/notification_utils/notification_utils.js +++ b/src/services/notification_utils/notification_utils.js @@ -23,6 +23,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/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 4ecb348efcea9664e259d3036aa5e289e2217942..cb241e3316ed99f6047e558faa400b1d1f32d0dc 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -17,7 +17,7 @@ const update = ({ store, notifications, older }) => { store.dispatch('addNewNotifications', { notifications, older }) } -const fetchAndUpdate = ({ store, credentials, older = false }) => { +const fetchAndUpdate = ({ store, credentials, older = false, since }) => { const args = { credentials } const { getters } = store const rootState = store.rootState || store.state @@ -35,8 +35,10 @@ const fetchAndUpdate = ({ store, credentials, older = false }) => { return fetchNotifications({ store, args, older }) } else { // fetch new notifications - if (timelineData.maxId !== Number.POSITIVE_INFINITY) { + if (since === undefined && timelineData.maxId !== Number.POSITIVE_INFINITY) { args['since'] = timelineData.maxId + } else if (since !== null) { + args['since'] = since } const result = fetchNotifications({ store, args, older }) diff --git a/src/services/ruffle_service/ruffle_service.js b/src/services/ruffle_service/ruffle_service.js new file mode 100644 index 0000000000000000000000000000000000000000..7411dd962d22c90406d3f9dd091d3ee9a574924b --- /dev/null +++ b/src/services/ruffle_service/ruffle_service.js @@ -0,0 +1,40 @@ +const createRuffleService = () => { + let ruffleInstance = null + + const getRuffle = () => new Promise((resolve, reject) => { + if (ruffleInstance) { + resolve(ruffleInstance) + return + } + // Ruffle needs these to be set before it's loaded + // https://github.com/ruffle-rs/ruffle/issues/3952 + window.RufflePlayer = {} + window.RufflePlayer.config = { + polyfills: false, + publicPath: '/static/ruffle' + } + + // Currently it's seems like a better way of loading ruffle + // because it needs the wasm publically accessible, but it needs path to it + // and filename of wasm seems to be pseudo-randomly generated (is it a hash?) + const script = document.createElement('script') + // see webpack config, using CopyPlugin to copy it from node_modules + // provided via ruffle-mirror + script.src = '/static/ruffle/ruffle.js' + script.type = 'text/javascript' + script.onerror = (e) => { reject(e) } + script.onabort = (e) => { reject(e) } + script.oncancel = (e) => { reject(e) } + script.onload = () => { + ruffleInstance = window.RufflePlayer + resolve(ruffleInstance) + } + document.body.appendChild(script) + }) + + return { getRuffle } +} + +const RuffleService = createRuffleService() + +export default RuffleService diff --git a/src/services/style_setter/style_setter.js b/src/services/style_setter/style_setter.js index 0fb8f92aaff6fa60b53c88f71354ae1c10b651ff..f75e69168f7c7c63253b6251413496e91c0905a7 100644 --- a/src/services/style_setter/style_setter.js +++ b/src/services/style_setter/style_setter.js @@ -244,7 +244,7 @@ export const generateShadows = (input, colors) => { } const cleanInputShadows = Object.fromEntries( - Object.entries(input.shadows) + Object.entries(input.shadows || {}) .map(([name, shadowSlot]) => [ name, // defaulting color to black to avoid potential problems @@ -380,7 +380,7 @@ export const colors2to3 = (colors) => { */ export const shadows2to3 = (shadows, opacity) => { return Object.entries(shadows).reduce((shadowsAcc, [slotName, shadowDefs]) => { - const isDynamic = ({ color }) => color.startsWith('--') + const isDynamic = ({ color = '#000000' }) => color.startsWith('--') const getOpacity = ({ color }) => opacity[getOpacitySlot(color.substring(2).split(',')[0])] const newShadow = shadowDefs.reduce((shadowAcc, def) => [ ...shadowAcc, diff --git a/src/services/theme_data/pleromafe.js b/src/services/theme_data/pleromafe.js index bec1eebdfc08246e3c426dc264b3670194cdca81..c2983be705158907979ff9b38abdef1d5f20c74d 100644 --- a/src/services/theme_data/pleromafe.js +++ b/src/services/theme_data/pleromafe.js @@ -369,6 +369,12 @@ export const SLOT_INHERITANCE = { textColor: 'preserve' }, + postCyantext: { + depends: ['cBlue'], + layer: 'bg', + textColor: 'preserve' + }, + border: { depends: ['fg'], opacity: 'border', @@ -616,6 +622,23 @@ export const SLOT_INHERITANCE = { textColor: true }, + alertSuccess: { + depends: ['cGreen'], + opacity: 'alert' + }, + alertSuccessText: { + depends: ['text'], + layer: 'alert', + variant: 'alertSuccess', + textColor: true + }, + alertSuccessPanelText: { + depends: ['panelText'], + layer: 'alertPanel', + variant: 'alertSuccess', + textColor: true + }, + alertNeutral: { depends: ['text'], opacity: 'alert' @@ -656,6 +679,17 @@ export const SLOT_INHERITANCE = { textColor: true }, + alertPopupSuccess: { + depends: ['alertSuccess'], + opacity: 'alertPopup' + }, + alertPopupSuccessText: { + depends: ['alertSuccessText'], + layer: 'popover', + variant: 'alertPopupSuccess', + textColor: true + }, + alertPopupNeutral: { depends: ['alertNeutral'], opacity: 'alertPopup' diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 921df3edac304e8c80ee8483d778107a7f88a372..46bba41a02f22f32c2747e002e9915dbf591ef87 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -23,7 +23,8 @@ const fetchAndUpdate = ({ showImmediately = false, userId = false, tag = false, - until + until, + since }) => { const args = { timeline, credentials } const rootState = store.rootState || store.state @@ -35,7 +36,11 @@ const fetchAndUpdate = ({ if (older) { args['until'] = until || timelineData.minId } else { - args['since'] = timelineData.maxId + if (since === undefined) { + args['since'] = timelineData.maxId + } else if (since !== null) { + args['since'] = since + } } args['userId'] = userId diff --git a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js b/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js deleted file mode 100644 index de6f20ef0b38e540586e1dedaf6d0e1ae0b772d7..0000000000000000000000000000000000000000 --- a/src/services/tiny_post_html_processor/tiny_post_html_processor.service.js +++ /dev/null @@ -1,94 +0,0 @@ -/** - * This is a tiny purpose-built HTML parser/processor. This basically detects any type of visual newline and - * allows it to be processed, useful for greentexting, mostly - * - * known issue: doesn't handle CDATA so nested CDATA might not work well - * - * @param {Object} input - input data - * @param {(string) => string} processor - function that will be called on every line - * @return {string} processed html - */ -export const processHtml = (html, processor) => { - const handledTags = new Set(['p', 'br', 'div']) - const openCloseTags = new Set(['p', 'div']) - - let buffer = '' // Current output buffer - const level = [] // How deep we are in tags and which tags were there - let textBuffer = '' // Current line content - let tagBuffer = null // Current tag buffer, if null = we are not currently reading a tag - - // Extracts tag name from tag, i.e. <span a="b"> => span - const getTagName = (tag) => { - const result = /(?:<\/(\w+)>|<(\w+)\s?[^/]*?\/?>)/gi.exec(tag) - return result && (result[1] || result[2]) - } - - const flush = () => { // Processes current line buffer, adds it to output buffer and clears line buffer - if (textBuffer.trim().length > 0) { - buffer += processor(textBuffer) - } else { - buffer += textBuffer - } - textBuffer = '' - } - - const handleBr = (tag) => { // handles single newlines/linebreaks/selfclosing - flush() - buffer += tag - } - - const handleOpen = (tag) => { // handles opening tags - flush() - buffer += tag - level.push(tag) - } - - const handleClose = (tag) => { // handles closing tags - flush() - buffer += tag - if (level[level.length - 1] === tag) { - level.pop() - } - } - - for (let i = 0; i < html.length; i++) { - const char = html[i] - if (char === '<' && tagBuffer === null) { - tagBuffer = char - } else if (char !== '>' && tagBuffer !== null) { - tagBuffer += char - } else if (char === '>' && tagBuffer !== null) { - tagBuffer += char - const tagFull = tagBuffer - tagBuffer = null - const tagName = getTagName(tagFull) - if (handledTags.has(tagName)) { - if (tagName === 'br') { - handleBr(tagFull) - } else if (openCloseTags.has(tagName)) { - if (tagFull[1] === '/') { - handleClose(tagFull) - } else if (tagFull[tagFull.length - 2] === '/') { - // self-closing - handleBr(tagFull) - } else { - handleOpen(tagFull) - } - } - } else { - textBuffer += tagFull - } - } else if (char === '\n') { - handleBr(char) - } else { - textBuffer += char - } - } - if (tagBuffer) { - textBuffer += tagBuffer - } - - flush() - - return buffer -} diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js index b91c0f784d8f048232e07d73ce7863d6425fccbf..3b07592e81b78f93816ab17e382d7dcc3c5cdfd1 100644 --- a/src/services/user_highlighter/user_highlighter.js +++ b/src/services/user_highlighter/user_highlighter.js @@ -8,6 +8,11 @@ const highlightStyle = (prefs) => { const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})` const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)` const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)` + const customProps = { + '--____highlight-solidColor': solidColor, + '--____highlight-tintColor': tintColor, + '--____highlight-tintColor2': tintColor2 + } if (type === 'striped') { return { backgroundImage: [ @@ -17,11 +22,13 @@ const highlightStyle = (prefs) => { `${tintColor2} 20px,`, `${tintColor2} 40px` ].join(' '), - backgroundPosition: '0 0' + backgroundPosition: '0 0', + ...customProps } } else if (type === 'solid') { return { - backgroundColor: tintColor2 + backgroundColor: tintColor2, + ...customProps } } else if (type === 'side') { return { @@ -31,7 +38,8 @@ const highlightStyle = (prefs) => { `${solidColor} 2px,`, `transparent 6px` ].join(' '), - backgroundPosition: '0 0' + backgroundPosition: '0 0', + ...customProps } } } diff --git a/static/config.json b/static/config.json index f59e645ac65ad316d437ea9c8296a92ec4f37372..53a4be8236f5d894671b2bd5213cf2e9c0fd24f8 100644 --- a/static/config.json +++ b/static/config.json @@ -2,7 +2,6 @@ "alwaysShowSubjectInput": true, "background": "/static/aurora_borealis.jpg", "collapseMessageWithSubject": false, - "disableChat": false, "greentext": false, "hideFilteredStatuses": false, "hideMutedPosts": false, diff --git a/test/unit/specs/components/rich_content.spec.js b/test/unit/specs/components/rich_content.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..30c66a33bd3a9de33bdbd0bc4ebd0a7b2c4dc957 --- /dev/null +++ b/test/unit/specs/components/rich_content.spec.js @@ -0,0 +1,557 @@ +import { mount, shallowMount, createLocalVue } from '@vue/test-utils' +import RichContent from 'src/components/rich_content/rich_content.jsx' + +const localVue = createLocalVue() +const attentions = [] + +const makeMention = (who) => { + attentions.push({ statusnet_profile_url: `https://fake.tld/@${who}` }) + return `<span class="h-card"><a class="u-url mention" href="https://fake.tld/@${who}">@<span>${who}</span></a></span>` +} +const p = (...data) => `<p>${data.join('')}</p>` +const compwrap = (...data) => `<span class="RichContent">${data.join('')}</span>` +const mentionsLine = (times) => [ + '<mentionsline-stub mentions="', + new Array(times).fill('[object Object]').join(','), + '"></mentionsline-stub>' +].join('') + +describe('RichContent', () => { + it('renders simple post without exploding', () => { + const html = p('Hello world!') + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(html)) + }) + + it('unescapes everything as needed', () => { + const html = [ + p('Testing 'em all'), + 'Testing 'em all' + ].join('') + const expected = [ + p('Testing \'em all'), + 'Testing \'em all' + ].join('') + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('replaces mention with mentionsline', () => { + const html = p( + makeMention('John'), + ' how are you doing today?' + ) + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(p( + mentionsLine(1), + ' how are you doing today?' + ))) + }) + + it('replaces mentions at the end of the hellpost', () => { + const html = [ + p('How are you doing today, fine gentlemen?'), + p( + makeMention('John'), + makeMention('Josh'), + makeMention('Jeremy') + ) + ].join('') + const expected = [ + p( + 'How are you doing today, fine gentlemen?' + ), + // TODO fix this extra line somehow? + p( + '<mentionsline-stub mentions="', + '[object Object],', + '[object Object],', + '[object Object]', + '"></mentionsline-stub>' + ) + ].join('') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('Does not touch links if link handling is disabled', () => { + const html = [ + [ + makeMention('Jack'), + 'let\'s meet up with ', + makeMention('Janet') + ].join(''), + [ + makeMention('John'), + makeMention('Josh'), + makeMention('Jeremy') + ].join('') + ].join('\n') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(html)) + }) + + it('Adds greentext and cyantext to the post', () => { + const html = [ + '>preordering videogames', + '>any year' + ].join('\n') + const expected = [ + '<span class="greentext">>preordering videogames</span>', + '<span class="greentext">>any year</span>' + ].join('\n') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('Does not add greentext and cyantext if setting is set to false', () => { + const html = [ + '>preordering videogames', + '>any year' + ].join('\n') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: false, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(html)) + }) + + it('Adds emoji to post', () => { + const html = p('Ebin :DDDD :spurdo:') + const expected = p( + 'Ebin :DDDD ', + '<anonymous-stub alt=":spurdo:" src="about:blank" title=":spurdo:" class="emoji img"></anonymous-stub>' + ) + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: false, + emoji: [{ url: 'about:blank', shortcode: 'spurdo' }], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('Doesn\'t add nonexistent emoji to post', () => { + const html = p('Lol :lol:') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: false, + greentext: false, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(html)) + }) + + it('Greentext + last mentions', () => { + const html = [ + '>quote', + makeMention('lol'), + '>quote', + '>quote' + ].join('\n') + const expected = [ + '<span class="greentext">>quote</span>', + mentionsLine(1), + '<span class="greentext">>quote</span>', + '<span class="greentext">>quote</span>' + ].join('\n') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('One buggy example', () => { + const html = [ + 'Bruh', + 'Bruh', + [ + makeMention('foo'), + makeMention('bar'), + makeMention('baz') + ].join(''), + 'Bruh' + ].join('<br>') + const expected = [ + 'Bruh', + 'Bruh', + mentionsLine(3), + 'Bruh' + ].join('<br>') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('buggy example/hashtags', () => { + const html = [ + '<p>', + '<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg">', + 'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>', + ' <a class="hashtag" data-tag="nou" href="https://shitposter.club/tag/nou">', + '#nou</a>', + ' <a class="hashtag" data-tag="screencap" href="https://shitposter.club/tag/screencap">', + '#screencap</a>', + ' </p>' + ].join('') + const expected = [ + '<p>', + '<a href="http://macrochan.org/images/N/H/NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg" target="_blank">', + 'NHCMDUXJPPZ6M3Z2CQ6D2EBRSWGE7MZY.jpg</a>', + ' <hashtaglink-stub url="https://shitposter.club/tag/nou" content="#nou" tag="nou">', + '</hashtaglink-stub>', + ' <hashtaglink-stub url="https://shitposter.club/tag/screencap" content="#screencap" tag="screencap">', + '</hashtaglink-stub>', + ' </p>' + ].join('') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('rich contents of a mention are handled properly', () => { + attentions.push({ statusnet_profile_url: 'lol' }) + const html = [ + p( + '<a href="lol" class="mention">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>' + ), + p( + 'Testing' + ) + ].join('') + const expected = [ + p( + '<span class="MentionsLine">', + '<span class="MentionLink mention-link">', + '<a href="lol" target="_blank" class="original">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + '<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display + '</span>', + '<!---->', // v-if placeholder, mentionsline's extra mentions and stuff + '</span>' + ), + p( + 'Testing' + ) + ].join('') + + const wrapper = mount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('rich contents of nested mentions are handled properly', () => { + attentions.push({ statusnet_profile_url: 'lol' }) + const html = [ + p( + '<span class="poast-style">', + '<a href="lol" class="mention">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + ' ', + '<a href="lol" class="mention">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + '</span>' + ), + p( + 'Testing' + ) + ].join('') + const expected = [ + p( + '<span class="poast-style">', + '<span class="MentionsLine">', + '<span class="MentionLink mention-link">', + '<a href="lol" target="_blank" class="original">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + '<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display + '</span>', + '<span class="MentionLink mention-link">', + '<a href="lol" target="_blank" class="original">', + '<span>', + 'https://</span>', + '<span>', + 'lol.tld/</span>', + '<span>', + '</span>', + '</a>', + '<!---->', // v-if placeholder, mentionlink's "new" (i.e. rich) display + '</span>', + '<!---->', // v-if placeholder, mentionsline's extra mentions and stuff + '</span>', + '</span>' + ), + ' ', + p( + 'Testing' + ) + ].join('') + + const wrapper = mount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it('rich contents of a link are handled properly', () => { + const html = [ + '<p>', + 'Freenode is dead.</p>', + '<p>', + '<a href="https://isfreenodedeadyet.com/">', + '<span>', + 'https://</span>', + '<span>', + 'isfreenodedeadyet.com/</span>', + '<span>', + '</span>', + '</a>', + '</p>' + ].join('') + const expected = [ + '<p>', + 'Freenode is dead.</p>', + '<p>', + '<a href="https://isfreenodedeadyet.com/" target="_blank">', + '<span>', + 'https://</span>', + '<span>', + 'isfreenodedeadyet.com/</span>', + '<span>', + '</span>', + '</a>', + '</p>' + ].join('') + + const wrapper = shallowMount(RichContent, { + localVue, + propsData: { + attentions, + handleLinks: true, + greentext: true, + emoji: [], + html + } + }) + + expect(wrapper.html()).to.eql(compwrap(expected)) + }) + + it.skip('[INFORMATIVE] Performance testing, 10 000 simple posts', () => { + const amount = 20 + + const onePost = p( + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + makeMention('Lain'), + ' i just landed in l a where are you' + ) + + const TestComponent = { + template: ` + <div v-if="!vhtml"> + ${new Array(amount).fill(`<RichContent html="${onePost}" :greentext="true" :handleLinks="handeLinks" :emoji="[]" :attentions="attentions"/>`)} + </div> + <div v-else="vhtml"> + ${new Array(amount).fill(`<div v-html="${onePost}"/>`)} + </div> + `, + props: ['handleLinks', 'attentions', 'vhtml'] + } + console.log(1) + + const ptest = (handleLinks, vhtml) => { + const t0 = performance.now() + + const wrapper = mount(TestComponent, { + localVue, + propsData: { + attentions, + handleLinks, + vhtml + } + }) + + const t1 = performance.now() + + wrapper.destroy() + + const t2 = performance.now() + + return `Mount: ${t1 - t0}ms, destroy: ${t2 - t1}ms, avg ${(t1 - t0) / amount}ms - ${(t2 - t1) / amount}ms per item` + } + + console.log(`${amount} items with links handling:`) + console.log(ptest(true)) + console.log(`${amount} items without links handling:`) + console.log(ptest(false)) + console.log(`${amount} items plain v-html:`) + console.log(ptest(false, true)) + }) +}) diff --git a/test/unit/specs/components/timeline.spec.js b/test/unit/specs/components/timeline.spec.js deleted file mode 100644 index 0c8674a84456c7b20ddb247edb6306062d4b3607..0000000000000000000000000000000000000000 --- a/test/unit/specs/components/timeline.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import { getExcludedStatusIdsByPinning } from 'src/components/timeline/timeline.js' - -describe('Timeline', () => { - describe('getExcludedStatusIdsByPinning', () => { - const mockStatuses = (ids) => ids.map(id => ({ id })) - - it('should return only members of both pinnedStatusIds and ids of the given statuses', () => { - const statusIds = [1, 2, 3, 4] - const statuses = mockStatuses(statusIds) - const pinnedStatusIds = [1, 3, 5] - const result = getExcludedStatusIdsByPinning(statuses, pinnedStatusIds) - result.forEach(item => { - expect(item).to.be.oneOf(statusIds) - expect(item).to.be.oneOf(pinnedStatusIds) - }) - }) - - it('should return ids of pinned statuses not posted before any unpinned status', () => { - const pinnedStatusIdSet1 = ['PINNED1', 'PINNED2'] - const pinnedStatusIdSet2 = ['PINNED3', 'PINNED4'] - const pinnedStatusIds = [...pinnedStatusIdSet1, ...pinnedStatusIdSet2] - const statusIds = [...pinnedStatusIdSet1, 'UNPINNED1', ...pinnedStatusIdSet2] - const statuses = mockStatuses(statusIds) - expect(getExcludedStatusIdsByPinning(statuses, pinnedStatusIds)).to.eql(pinnedStatusIdSet1) - }) - }) -}) diff --git a/test/unit/specs/components/user_profile.spec.js b/test/unit/specs/components/user_profile.spec.js index 80092b410cbc05ffd527535d011be6fffe874851..142db73ce66e25dd35e3a7cfd0dae7d76154f7cc 100644 --- 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 index 0251cae78204c433688d499b6e33a5481de14513..fbbca4361bd46ba1f37694e9a569407e563e9f0b 100644 --- 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 index a3f49b2cefe92f6aebb2d06cde3d06cc727d1e83..03fb32c950f8db5e820d1b61dfce25e2c0f16152 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -1,4 +1,4 @@ -import { parseStatus, parseUser, parseNotification, addEmojis, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' +import { parseStatus, parseUser, parseNotification, parseLinkHeaderPagination } from '../../../../../src/services/entity_normalizer/entity_normalizer.service.js' import mastoapidata from '../../../../fixtures/mastoapi.json' import qvitterapidata from '../../../../fixtures/statuses.json' @@ -23,7 +23,6 @@ const makeMockStatusQvitter = (overrides = {}) => { repeat_num: 0, repeated: false, statusnet_conversation_id: '16300488', - statusnet_html: '<p>haha benis</p>', summary: null, tags: [], text: 'haha benis', @@ -232,22 +231,6 @@ describe('API Entities normalizer', () => { expect(parsedRepeat).to.have.property('retweeted_status') expect(parsedRepeat).to.have.deep.property('retweeted_status.id', 'deadbeef') }) - - it('adds emojis to post content', () => { - const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), content: 'Makes you think :thinking:' }) - - const parsedPost = parseStatus(post) - - expect(parsedPost).to.have.property('statusnet_html').that.contains('<img') - }) - - it('adds emojis to subject line', () => { - const post = makeMockStatusMasto({ emojis: makeMockEmojiMasto(), spoiler_text: 'CW: 300 IQ :thinking:' }) - - const parsedPost = parseStatus(post) - - expect(parsedPost).to.have.property('summary_html').that.contains('<img') - }) }) }) @@ -261,35 +244,6 @@ describe('API Entities normalizer', () => { expect(parseUser(remote)).to.have.property('is_local', false) }) - it('adds emojis to user name', () => { - const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), display_name: 'The :thinking: thinker' }) - - const parsedUser = parseUser(user) - - expect(parsedUser).to.have.property('name_html').that.contains('<img') - }) - - it('adds emojis to user bio', () => { - const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), note: 'Hello i like to :thinking: a lot' }) - - const parsedUser = parseUser(user) - - expect(parsedUser).to.have.property('description_html').that.contains('<img') - }) - - it('adds emojis to user profile fields', () => { - const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: ':thinking:', value: ':image:' }] }) - - const parsedUser = parseUser(user) - - expect(parsedUser).to.have.property('fields_html').to.be.an('array') - - const field = parsedUser.fields_html[0] - - expect(field).to.have.property('name').that.contains('<img') - expect(field).to.have.property('value').that.contains('<img') - }) - it('removes html tags from user profile fields', () => { const user = makeMockUserMasto({ emojis: makeMockEmojiMasto(), fields: [{ name: 'user', value: '<a rel="me" href="https://example.com/@user">@user</a>' }] }) @@ -315,7 +269,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') }) }) @@ -355,41 +309,6 @@ describe('API Entities normalizer', () => { }) }) - describe('MastoAPI emoji adder', () => { - const emojis = makeMockEmojiMasto() - const imageHtml = '<img src="https://example.com/image.png" alt=":image:" title=":image:" class="emoji" />' - .replace(/"/g, '\'') - const thinkHtml = '<img src="https://example.com/think.png" alt=":thinking:" title=":thinking:" class="emoji" />' - .replace(/"/g, '\'') - - it('correctly replaces shortcodes in supplied string', () => { - const result = addEmojis('This post has :image: emoji and :thinking: emoji', emojis) - expect(result).to.include(thinkHtml) - expect(result).to.include(imageHtml) - }) - - it('handles consecutive emojis correctly', () => { - const result = addEmojis('Lelel emoji spam :thinking::thinking::thinking::thinking:', emojis) - expect(result).to.include(thinkHtml + thinkHtml + thinkHtml + thinkHtml) - }) - - it('Doesn\'t replace nonexistent emojis', () => { - const result = addEmojis('Admin add the :tenshi: emoji', emojis) - expect(result).to.equal('Admin add the :tenshi: emoji') - }) - - it('Doesn\'t blow up on regex special characters', () => { - const emojis = makeMockEmojiMasto([{ - shortcode: 'c++' - }, { - shortcode: '[a-z] {|}*' - }]) - const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis) - expect(result).to.include('title=\':c++:\'') - expect(result).to.include('title=\':[a-z] {|}*:\'') - }) - }) - describe('Link header pagination', () => { it('Parses min and max ids as integers', () => { const linkHeader = '<https://example.com/api/v1/notifications?max_id=861676>; rel="next", <https://example.com/api/v1/notifications?min_id=861741>; rel="prev"' diff --git a/test/unit/specs/services/html_converter/html_line_converter.spec.js b/test/unit/specs/services/html_converter/html_line_converter.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..86bd7e8b0130ef3c03a928ffc77dda97b69f4858 --- /dev/null +++ b/test/unit/specs/services/html_converter/html_line_converter.spec.js @@ -0,0 +1,171 @@ +import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js' + +const greentextHandle = new Set(['p', 'div']) +const mapOnlyText = (processor) => (input) => { + if (input.text && input.level.every(l => greentextHandle.has(l))) { + return processor(input.text) + } else if (input.text) { + return input.text + } else { + return input + } +} + +describe('html_line_converter', () => { + describe('with processor that keeps original line should not make any changes to HTML when', () => { + const processorKeep = (line) => line + it('fed with regular HTML with newlines', () => { + const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with possibly broken HTML with invalid tags/composition', () => { + const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with very broken HTML with broken composition', () => { + const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with sorta valid HTML but tags aren\'t closed', () => { + const inputOutput = 'just leaving a <div> hanging' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with not really HTML at this point... tags that aren\'t finished', () => { + const inputOutput = 'do you expect me to finish this <div class=' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with dubiously valid HTML (p within p and also div inside p)', () => { + const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with maybe valid HTML? self-closing divs and ps', () => { + const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with valid XHTML containing a CDATA', () => { + const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + + it('fed with some recognized but not handled elements', () => { + const inputOutput = 'testing images\n\n<img src="benis.png">' + const result = convertHtmlToLines(inputOutput) + const comparableResult = result.map(mapOnlyText(processorKeep)).join('') + expect(comparableResult).to.eql(inputOutput) + }) + }) + describe('with processor that replaces lines with word "_" should match expected line when', () => { + const processorReplace = (line) => '_' + it('fed with regular HTML with newlines', () => { + const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>' + const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with possibly broken HTML with invalid tags/composition', () => { + const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>' + const output = '_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with very broken HTML with broken composition', () => { + const input = '</p> lmao what </div> whats going on <div> wha <p>' + const output = '_<div>_<p>' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with sorta valid HTML but tags aren\'t closed', () => { + const input = 'just leaving a <div> hanging' + const output = '_<div>_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with not really HTML at this point... tags that aren\'t finished', () => { + const input = 'do you expect me to finish this <div class=' + const output = '_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with dubiously valid HTML (p within p and also div inside p)', () => { + const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>' + const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with maybe valid HTML? (XHTML) self-closing divs and ps', () => { + const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?' + const output = '_<div class="what"/>_<p aria-label="wtf"/>_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('fed with valid XHTML containing a CDATA', () => { + const input = 'Yes, it is me, <![CDATA[DIO]]>' + const output = '_' + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + + it('Testing handling ignored blocks', () => { + const input = ` + <pre><code>> rei = "0" + '0' + > rei == 0 + true + > rei == null + false</code></pre><blockquote>That, christian-like JS diagram but it’s evangelion instead.</blockquote> + ` + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(input) + }) + it('Testing handling ignored blocks 2', () => { + const input = ` + <blockquote>An SSL error has happened.</blockquote><p>Shakespeare</p> + ` + const output = ` + <blockquote>An SSL error has happened.</blockquote><p>_</p> + ` + const result = convertHtmlToLines(input) + const comparableResult = result.map(mapOnlyText(processorReplace)).join('') + expect(comparableResult).to.eql(output) + }) + }) +}) diff --git a/test/unit/specs/services/html_converter/html_tree_converter.spec.js b/test/unit/specs/services/html_converter/html_tree_converter.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7283021b21a07ff275f8b04e7934fb8d2633676c --- /dev/null +++ b/test/unit/specs/services/html_converter/html_tree_converter.spec.js @@ -0,0 +1,132 @@ +import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js' + +describe('html_tree_converter', () => { + describe('convertHtmlToTree', () => { + it('converts html into a tree structure', () => { + const input = '1 <p>2</p> <b>3<img src="a">4</b>5' + expect(convertHtmlToTree(input)).to.eql([ + '1 ', + [ + '<p>', + ['2'], + '</p>' + ], + ' ', + [ + '<b>', + [ + '3', + ['<img src="a">'], + '4' + ], + '</b>' + ], + '5' + ]) + }) + it('converts html to tree while preserving tag formatting', () => { + const input = '1 <p >2</p><b >3<img src="a">4</b>5' + expect(convertHtmlToTree(input)).to.eql([ + '1 ', + [ + '<p >', + ['2'], + '</p>' + ], + [ + '<b >', + [ + '3', + ['<img src="a">'], + '4' + ], + '</b>' + ], + '5' + ]) + }) + it('converts semi-broken html', () => { + const input = '1 <br> 2 <p> 42' + expect(convertHtmlToTree(input)).to.eql([ + '1 ', + ['<br>'], + ' 2 ', + [ + '<p>', + [' 42'] + ] + ]) + }) + it('realistic case 1', () => { + const input = '<p><span class="h-card"><a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">@<span>benis</span></a></span> <span class="h-card"><a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">@<span>hj</span></a></span> nice</p>' + expect(convertHtmlToTree(input)).to.eql([ + [ + '<p>', + [ + [ + '<span class="h-card">', + [ + [ + '<a class="u-url mention" data-user="9wRC6T2ZZiKWJ0vUi8" href="https://cawfee.club/users/benis" rel="ugc">', + [ + '@', + [ + '<span>', + [ + 'benis' + ], + '</span>' + ] + ], + '</a>' + ] + ], + '</span>' + ], + ' ', + [ + '<span class="h-card">', + [ + [ + '<a class="u-url mention" data-user="194" href="https://shigusegubu.club/users/hj" rel="ugc">', + [ + '@', + [ + '<span>', + [ + 'hj' + ], + '</span>' + ] + ], + '</a>' + ] + ], + '</span>' + ], + ' nice' + ], + '</p>' + ] + ]) + }) + it('realistic case 2', () => { + const inputOutput = 'Country improv: give me a city<br/>Audience: Memphis<br/>Improv troupe: come on, a better one<br/>Audience: el paso' + expect(convertHtmlToTree(inputOutput)).to.eql([ + 'Country improv: give me a city', + [ + '<br/>' + ], + 'Audience: Memphis', + [ + '<br/>' + ], + 'Improv troupe: come on, a better one', + [ + '<br/>' + ], + 'Audience: el paso' + ]) + }) + }) +}) diff --git a/test/unit/specs/services/html_converter/utility.spec.js b/test/unit/specs/services/html_converter/utility.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..cf6fd99b4a65d3c00d19856b52f1918011c756a3 --- /dev/null +++ b/test/unit/specs/services/html_converter/utility.spec.js @@ -0,0 +1,37 @@ +import { processTextForEmoji, getAttrs } from 'src/services/html_converter/utility.service.js' + +describe('html_converter utility', () => { + describe('processTextForEmoji', () => { + it('processes all emoji in text', () => { + const input = 'Hello from finland! :lol: We have best water! :lmao:' + const emojis = [ + { shortcode: 'lol', src: 'LOL' }, + { shortcode: 'lmao', src: 'LMAO' } + ] + const processor = ({ shortcode, src }) => ({ shortcode, src }) + expect(processTextForEmoji(input, emojis, processor)).to.eql([ + 'Hello from finland! ', + { shortcode: 'lol', src: 'LOL' }, + ' We have best water! ', + { shortcode: 'lmao', src: 'LMAO' } + ]) + }) + it('leaves text as is', () => { + const input = 'Number one: that\'s terror' + const emojis = [] + const processor = ({ shortcode, src }) => ({ shortcode, src }) + expect(processTextForEmoji(input, emojis, processor)).to.eql([ + 'Number one: that\'s terror' + ]) + }) + }) + + describe('getAttrs', () => { + it('extracts arguments from tag', () => { + const input = '<img src="boop" cool ebin=\'true\'>' + const output = { src: 'boop', cool: true, ebin: 'true' } + + expect(getAttrs(input)).to.eql(output) + }) + }) +}) diff --git a/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js b/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js deleted file mode 100644 index f301429d066e9fa6f8ea21adc72081320889ea07..0000000000000000000000000000000000000000 --- a/test/unit/specs/services/tiny_post_html_processor/tiny_post_html_processor.spec.js +++ /dev/null @@ -1,96 +0,0 @@ -import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js' - -describe('TinyPostHTMLProcessor', () => { - describe('with processor that keeps original line should not make any changes to HTML when', () => { - const processorKeep = (line) => line - it('fed with regular HTML with newlines', () => { - const inputOutput = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with possibly broken HTML with invalid tags/composition', () => { - const inputOutput = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with very broken HTML with broken composition', () => { - const inputOutput = '</p> lmao what </div> whats going on <div> wha <p>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with sorta valid HTML but tags aren\'t closed', () => { - const inputOutput = 'just leaving a <div> hanging' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with not really HTML at this point... tags that aren\'t finished', () => { - const inputOutput = 'do you expect me to finish this <div class=' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with dubiously valid HTML (p within p and also div inside p)', () => { - const inputOutput = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with maybe valid HTML? self-closing divs and ps', () => { - const inputOutput = 'a <div class="what"/> what now <p aria-label="wtf"/> ?' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - - it('fed with valid XHTML containing a CDATA', () => { - const inputOutput = 'Yes, it is me, <![CDATA[DIO]]>' - expect(processHtml(inputOutput, processorKeep)).to.eql(inputOutput) - }) - }) - describe('with processor that replaces lines with word "_" should match expected line when', () => { - const processorReplace = (line) => '_' - it('fed with regular HTML with newlines', () => { - const input = '1<br/>2<p class="lol">3 4</p> 5 \n 6 <p > 7 <br> 8 </p> <br>\n<br/>' - const output = '_<br/>_<p class="lol">_</p>_\n_<p >_<br>_</p> <br>\n<br/>' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with possibly broken HTML with invalid tags/composition', () => { - const input = '<feeee dwdwddddddw> <i>ayy<b>lm</i>ao</b> </section>' - const output = '_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with very broken HTML with broken composition', () => { - const input = '</p> lmao what </div> whats going on <div> wha <p>' - const output = '</p>_</div>_<div>_<p>' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with sorta valid HTML but tags aren\'t closed', () => { - const input = 'just leaving a <div> hanging' - const output = '_<div>_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with not really HTML at this point... tags that aren\'t finished', () => { - const input = 'do you expect me to finish this <div class=' - const output = '_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with dubiously valid HTML (p within p and also div inside p)', () => { - const input = 'look ma <p> p \nwithin <p> p! </p> and a <br/><div>div!</div></p>' - const output = '_<p>_\n_<p>_</p>_<br/><div>_</div></p>' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with maybe valid HTML? self-closing divs and ps', () => { - const input = 'a <div class="what"/> what now <p aria-label="wtf"/> ?' - const output = '_<div class="what"/>_<p aria-label="wtf"/>_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - - it('fed with valid XHTML containing a CDATA', () => { - const input = 'Yes, it is me, <![CDATA[DIO]]>' - const output = '_' - expect(processHtml(input, processorReplace)).to.eql(output) - }) - }) -}) diff --git a/yarn.lock b/yarn.lock index 6682841b8df0c29d656201da33d600fd600f760f..9329cc3a230b263ff7193e881316e027981f93ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -936,6 +936,14 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@stylelint/postcss-css-in-js@^0.37.1": version "0.37.2" resolved "https://registry.yarnpkg.com/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz#7e5a84ad181f4234a2480803422a47b8749af3d2" @@ -961,6 +969,11 @@ resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== +"@types/json-schema@^7.0.6": + version "7.0.7" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" + integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== + "@types/minimist@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" @@ -998,156 +1011,236 @@ resolved "https://registry.yarnpkg.com/@ungap/event-target/-/event-target-0.1.0.tgz#88d527d40de86c4b0c99a060ca241d755999915b" integrity sha512-W2oyj0Fe1w/XhPZjkI3oUcDUAmu5P4qsdT2/2S8aMhtAWM/CE/jYWtji0pKNPDfxLI75fa5gWSEmnynKMNP/oA== -"@vue/babel-helper-vue-jsx-merge-props@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.0.0.tgz#048fe579958da408fb7a8b2a3ec050b50a661040" - integrity sha512-6tyf5Cqm4m6v7buITuwS+jHzPlIPxbFzEhXR5JGZpbrvOcp1hiQKckd305/3C7C36wFekNTQSxAtgeM0j0yoUw== +"@vue/babel-helper-vue-jsx-merge-props@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81" + integrity sha512-QOi5OW45e2R20VygMSNhyQHvpdUwQZqGPc748JLGCYEy+yp8fNFNdbNIGAgZmi9e+2JHPd6i6idRuqivyicIkA== -"@vue/babel-plugin-transform-vue-jsx@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.1.2.tgz#c0a3e6efc022e75e4247b448a8fc6b86f03e91c0" - integrity sha512-YfdaoSMvD1nj7+DsrwfTvTnhDXI7bsuh+Y5qWwvQXlD24uLgnsoww3qbiZvWf/EoviZMrvqkqN4CBw0W3BWUTQ== +"@vue/babel-plugin-transform-vue-jsx@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.2.1.tgz#646046c652c2f0242727f34519d917b064041ed7" + integrity sha512-HJuqwACYehQwh1fNT8f4kyzqlNMpBuUK4rSiSES5D4QsYncv5fxFsLyrxFPG2ksO7t5WP+Vgix6tt6yKClwPzA== dependencies: "@babel/helper-module-imports" "^7.0.0" "@babel/plugin-syntax-jsx" "^7.2.0" - "@vue/babel-helper-vue-jsx-merge-props" "^1.0.0" + "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1" html-tags "^2.0.0" lodash.kebabcase "^4.1.1" svg-tags "^1.0.0" -"@vue/test-utils@^1.0.0-beta.26": - version "1.0.0-beta.28" - resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5" - dependencies: - dom-event-types "^1.0.0" - lodash "^4.17.4" - -"@webassemblyjs/ast@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" +"@vue/babel-preset-jsx@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@vue/babel-preset-jsx/-/babel-preset-jsx-1.2.4.tgz#92fea79db6f13b01e80d3a0099e2924bdcbe4e87" + integrity sha512-oRVnmN2a77bYDJzeGSt92AuHXbkIxbf/XXSE3klINnh9AXBmVS1DGa1f0d+dDYpLfsAKElMnqKTQfKn7obcL4w== + dependencies: + "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + "@vue/babel-sugar-composition-api-inject-h" "^1.2.1" + "@vue/babel-sugar-composition-api-render-instance" "^1.2.4" + "@vue/babel-sugar-functional-vue" "^1.2.2" + "@vue/babel-sugar-inject-h" "^1.2.2" + "@vue/babel-sugar-v-model" "^1.2.3" + "@vue/babel-sugar-v-on" "^1.2.3" + +"@vue/babel-sugar-composition-api-inject-h@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.2.1.tgz#05d6e0c432710e37582b2be9a6049b689b6f03eb" + integrity sha512-4B3L5Z2G+7s+9Bwbf+zPIifkFNcKth7fQwekVbnOA3cr3Pq71q71goWr97sk4/yyzH8phfe5ODVzEjX7HU7ItQ== dependencies: - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/wast-parser" "1.8.5" - -"@webassemblyjs/floating-point-hex-parser@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" - -"@webassemblyjs/helper-api-error@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" - -"@webassemblyjs/helper-buffer@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@webassemblyjs/helper-code-frame@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" +"@vue/babel-sugar-composition-api-render-instance@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.2.4.tgz#e4cbc6997c344fac271785ad7a29325c51d68d19" + integrity sha512-joha4PZznQMsxQYXtR3MnTgCASC9u3zt9KfBxIeuI5g2gscpTsSKRDzWQt4aqNIpx6cv8On7/m6zmmovlNsG7Q== dependencies: - "@webassemblyjs/wast-printer" "1.8.5" - -"@webassemblyjs/helper-fsm@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@webassemblyjs/helper-module-context@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" +"@vue/babel-sugar-functional-vue@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.2.2.tgz#267a9ac8d787c96edbf03ce3f392c49da9bd2658" + integrity sha512-JvbgGn1bjCLByIAU1VOoepHQ1vFsroSA/QkzdiSs657V79q6OwEWLCQtQnEXD/rLTA8rRit4rMOhFpbjRFm82w== dependencies: - "@webassemblyjs/ast" "1.8.5" - mamacro "^0.0.3" - -"@webassemblyjs/helper-wasm-bytecode@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@webassemblyjs/helper-wasm-section@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" +"@vue/babel-sugar-inject-h@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.2.2.tgz#d738d3c893367ec8491dcbb669b000919293e3aa" + integrity sha512-y8vTo00oRkzQTgufeotjCLPAvlhnpSkcHFEp60+LJUwygGcd5Chrpn5480AQp/thrxVm8m2ifAk0LyFel9oCnw== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" + "@babel/plugin-syntax-jsx" "^7.2.0" -"@webassemblyjs/ieee754@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" +"@vue/babel-sugar-v-model@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.2.3.tgz#fa1f29ba51ebf0aa1a6c35fa66d539bc459a18f2" + integrity sha512-A2jxx87mySr/ulAsSSyYE8un6SIH0NWHiLaCWpodPCVOlQVODCaSpiR4+IMsmBr73haG+oeCuSvMOM+ttWUqRQ== dependencies: - "@xtuc/ieee754" "^1.2.0" + "@babel/plugin-syntax-jsx" "^7.2.0" + "@vue/babel-helper-vue-jsx-merge-props" "^1.2.1" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + camelcase "^5.0.0" + html-tags "^2.0.0" + svg-tags "^1.0.0" -"@webassemblyjs/leb128@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" +"@vue/babel-sugar-v-on@^1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.2.3.tgz#342367178586a69f392f04bfba32021d02913ada" + integrity sha512-kt12VJdz/37D3N3eglBywV8GStKNUhNrsxChXIV+o0MwVXORYuhDTHJRKPgLJRb/EY3vM2aRFQdxJBp9CLikjw== dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" + "@babel/plugin-syntax-jsx" "^7.2.0" + "@vue/babel-plugin-transform-vue-jsx" "^1.2.1" + camelcase "^5.0.0" -"@webassemblyjs/wasm-edit@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/helper-wasm-section" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" - "@webassemblyjs/wasm-opt" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - "@webassemblyjs/wast-printer" "1.8.5" - -"@webassemblyjs/wasm-gen@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" +"@vue/test-utils@^1.0.0-beta.26": + version "1.0.0-beta.28" + resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.0.0-beta.28.tgz#767c43413df8cde86128735e58923803e444b9a5" dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/ieee754" "1.8.5" - "@webassemblyjs/leb128" "1.8.5" - "@webassemblyjs/utf8" "1.8.5" + dom-event-types "^1.0.0" + lodash "^4.17.4" -"@webassemblyjs/wasm-opt@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" +"@webassemblyjs/ast@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" + integrity sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA== + dependencies: + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" + +"@webassemblyjs/floating-point-hex-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz#3c3d3b271bddfc84deb00f71344438311d52ffb4" + integrity sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA== + +"@webassemblyjs/helper-api-error@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz#203f676e333b96c9da2eeab3ccef33c45928b6a2" + integrity sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw== + +"@webassemblyjs/helper-buffer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz#a1442d269c5feb23fcbc9ef759dac3547f29de00" + integrity sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA== + +"@webassemblyjs/helper-code-frame@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz#647f8892cd2043a82ac0c8c5e75c36f1d9159f27" + integrity sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA== + dependencies: + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/helper-fsm@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz#c05256b71244214671f4b08ec108ad63b70eddb8" + integrity sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw== + +"@webassemblyjs/helper-module-context@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz#25d8884b76839871a08a6c6f806c3979ef712f07" + integrity sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g== + dependencies: + "@webassemblyjs/ast" "1.9.0" + +"@webassemblyjs/helper-wasm-bytecode@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz#4fed8beac9b8c14f8c58b70d124d549dd1fe5790" + integrity sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw== + +"@webassemblyjs/helper-wasm-section@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz#5a4138d5a6292ba18b04c5ae49717e4167965346" + integrity sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + +"@webassemblyjs/ieee754@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz#15c7a0fbaae83fb26143bbacf6d6df1702ad39e4" + integrity sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-buffer" "1.8.5" - "@webassemblyjs/wasm-gen" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" + "@xtuc/ieee754" "^1.2.0" -"@webassemblyjs/wasm-parser@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" +"@webassemblyjs/leb128@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.9.0.tgz#f19ca0b76a6dc55623a09cffa769e838fa1e1c95" + integrity sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-api-error" "1.8.5" - "@webassemblyjs/helper-wasm-bytecode" "1.8.5" - "@webassemblyjs/ieee754" "1.8.5" - "@webassemblyjs/leb128" "1.8.5" - "@webassemblyjs/utf8" "1.8.5" + "@xtuc/long" "4.2.2" -"@webassemblyjs/wast-parser@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" - dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/floating-point-hex-parser" "1.8.5" - "@webassemblyjs/helper-api-error" "1.8.5" - "@webassemblyjs/helper-code-frame" "1.8.5" - "@webassemblyjs/helper-fsm" "1.8.5" +"@webassemblyjs/utf8@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.9.0.tgz#04d33b636f78e6a6813227e82402f7637b6229ab" + integrity sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w== + +"@webassemblyjs/wasm-edit@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz#3fe6d79d3f0f922183aa86002c42dd256cfee9cf" + integrity sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/helper-wasm-section" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-opt" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + "@webassemblyjs/wast-printer" "1.9.0" + +"@webassemblyjs/wasm-gen@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz#50bc70ec68ded8e2763b01a1418bf43491a7a49c" + integrity sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wasm-opt@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz#2211181e5b31326443cc8112eb9f0b9028721a61" + integrity sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-buffer" "1.9.0" + "@webassemblyjs/wasm-gen" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + +"@webassemblyjs/wasm-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz#9d48e44826df4a6598294aa6c87469d642fff65e" + integrity sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-wasm-bytecode" "1.9.0" + "@webassemblyjs/ieee754" "1.9.0" + "@webassemblyjs/leb128" "1.9.0" + "@webassemblyjs/utf8" "1.9.0" + +"@webassemblyjs/wast-parser@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz#3031115d79ac5bd261556cecc3fa90a3ef451914" + integrity sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/floating-point-hex-parser" "1.9.0" + "@webassemblyjs/helper-api-error" "1.9.0" + "@webassemblyjs/helper-code-frame" "1.9.0" + "@webassemblyjs/helper-fsm" "1.9.0" "@xtuc/long" "4.2.2" -"@webassemblyjs/wast-printer@1.8.5": - version "1.8.5" - resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" +"@webassemblyjs/wast-printer@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz#4935d54c85fef637b00ce9f52377451d00d47899" + integrity sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/wast-parser" "1.8.5" + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/wast-parser" "1.9.0" "@xtuc/long" "4.2.2" "@xtuc/ieee754@^1.2.0": @@ -1176,18 +1269,19 @@ accepts@~1.3.5: mime-types "~2.1.18" negotiator "0.6.1" -acorn-dynamic-import@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" - acorn-jsx@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.1.tgz#32a064fd925429216a09b141102bfdd185fae40e" -acorn@^6.0.2, acorn@^6.0.5, acorn@^6.0.7: +acorn@^6.0.2, acorn@^6.0.7: version "6.1.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.1.1.tgz#7d25ae05bb8ad1f9b699108e1094ecd7884adc1f" +acorn@^6.4.1: + version "6.4.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" + integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== + after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" @@ -1222,6 +1316,11 @@ ajv-keywords@^3.1.0: version "3.4.0" resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.0.tgz#4b831e7b531415a7cc518cd404e73f6193c6349d" +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + ajv@^6.1.0, ajv@^6.9.1: version "6.10.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" @@ -1241,6 +1340,16 @@ ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -1307,6 +1416,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@~3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -1691,6 +1808,11 @@ binary-extensions@^1.0.0: version "1.12.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + blob@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" @@ -1699,9 +1821,10 @@ bluebird@^3.1.1, bluebird@^3.3.0: version "3.5.3" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" -bluebird@^3.5.3: - version "3.5.4" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.4.tgz#d6cc661595de30d5b3af5fcedd3c0b3ef6ec5714" +bluebird@^3.5.5: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -1767,7 +1890,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -1909,25 +2032,50 @@ bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" -cacache@^11.3.2: - version "11.3.2" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.2.tgz#2d81e308e3d258ca38125b676b98b2ac9ce69bfa" +cacache@^12.0.2: + version "12.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" + integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== dependencies: - bluebird "^3.5.3" + bluebird "^3.5.5" chownr "^1.1.1" figgy-pudding "^3.5.1" - glob "^7.1.3" + glob "^7.1.4" graceful-fs "^4.1.15" + infer-owner "^1.0.3" lru-cache "^5.1.1" mississippi "^3.0.0" mkdirp "^0.5.1" move-concurrently "^1.0.1" promise-inflight "^1.0.1" - rimraf "^2.6.2" + rimraf "^2.6.3" ssri "^6.0.1" unique-filename "^1.1.1" y18n "^4.0.0" +cacache@^15.0.5: + version "15.0.6" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.0.6.tgz#65a8c580fda15b59150fb76bf3f3a8e45d583099" + integrity sha512-g1WYDMct/jzW+JdWEyjaX2zoBkZ6ZT9VpOyp2I/VMtDsNLffNat3kqPFfi1eDRSK9/SuKGyORDHcQMcPF8sQ/w== + dependencies: + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -2117,7 +2265,7 @@ chardet@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" -chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3: +chokidar@^2.0.0, chokidar@^2.0.3: version "2.1.6" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" dependencies: @@ -2135,19 +2283,57 @@ chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3: optionalDependencies: fsevents "^1.2.7" +chokidar@^2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" + integrity sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chokidar@^3.4.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" + integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.5.0" + optionalDependencies: + fsevents "~2.3.1" + chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + chromatism@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/chromatism/-/chromatism-3.0.0.tgz#a7249d353c1e4f3577e444ac41171c4e2e624b12" -chrome-trace-event@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.0.tgz#45a91bd2c20c9411f0963b5aaeb9a1b95e09cc48" - dependencies: - tslib "^1.9.0" +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== chromedriver@^87.0.1: version "87.0.4" @@ -2350,9 +2536,10 @@ commander@2.9.0: dependencies: graceful-readlink ">= 1.0.0" -commander@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== commondir@^1.0.1: version "1.0.1" @@ -2464,6 +2651,23 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" +copy-webpack-plugin@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz#138cd9b436dbca0a6d071720d5414848992ec47e" + integrity sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA== + dependencies: + cacache "^15.0.5" + fast-glob "^3.2.4" + find-cache-dir "^3.3.1" + glob-parent "^5.1.1" + globby "^11.0.1" + loader-utils "^2.0.0" + normalize-path "^3.0.0" + p-limit "^3.0.2" + schema-utils "^3.0.0" + serialize-javascript "^5.0.1" + webpack-sources "^1.4.3" + core-js-compat@^3.4.7: version "3.4.8" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.4.8.tgz#f72e6a4ed76437ea710928f44615f926a81607d5" @@ -3053,6 +3257,11 @@ emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + encodeurl@~1.0.1, encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" @@ -3100,12 +3309,13 @@ engine.io@~3.2.0: engine.io-parser "~2.1.0" ws "~3.3.1" -enhanced-resolve@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" +enhanced-resolve@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" + integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== dependencies: graceful-fs "^4.1.2" - memory-fs "^0.4.0" + memory-fs "^0.5.0" tapable "^1.0.0" ent@~2.2.0: @@ -3572,6 +3782,18 @@ fast-glob@^3.1.1: micromatch "^4.0.2" picomatch "^2.2.1" +fast-glob@^3.2.4: + version "3.2.5" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" + integrity sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" + fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" @@ -3687,7 +3909,7 @@ find-cache-dir@^0.1.1: mkdirp "^0.5.1" pkg-dir "^1.0.0" -find-cache-dir@^2.0.0: +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" dependencies: @@ -3695,6 +3917,15 @@ find-cache-dir@^2.0.0: make-dir "^2.0.0" pkg-dir "^3.0.0" +find-cache-dir@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" + integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -3714,7 +3945,7 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@^4.1.0: +find-up@^4.0.0, find-up@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== @@ -3799,6 +4030,13 @@ fs-minipass@^1.2.5: dependencies: minipass "^2.2.1" +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + fs-write-stream-atomic@^1.0.8: version "1.0.10" resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" @@ -3819,6 +4057,11 @@ fsevents@^1.2.7: nan "^2.12.1" node-pre-gyp "^0.12.0" +fsevents@~2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + ftp@~0.3.10: version "0.3.10" resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" @@ -3920,6 +4163,13 @@ glob-parent@^5.1.0: dependencies: is-glob "^4.0.1" +glob-parent@^5.1.1, glob-parent@~5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + glob@7.0.5: version "7.0.5" resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.5.tgz#b4202a69099bbb4d292a7c1b95b6682b67ebdc95" @@ -3974,6 +4224,18 @@ glob@^7.1.2: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^7.1.4: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + global-modules@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -4404,6 +4666,11 @@ indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" +infer-owner@^1.0.3, infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -4526,6 +4793,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -4647,7 +4921,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" dependencies: @@ -5130,9 +5404,10 @@ loader-fs-cache@^1.0.0: find-cache-dir "^0.1.1" mkdirp "0.5.1" -loader-runner@^2.3.0: +loader-runner@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== loader-utils@^0.2.16, loader-utils@^0.2.3: version "0.2.17" @@ -5151,6 +5426,24 @@ loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^1.0.1" +loader-utils@^1.2.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" + integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +loader-utils@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.0.tgz#e4cace5b816d425a166b5f097e10cd12b36064b0" + integrity sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + localforage@^1.5.0: version "1.7.3" resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.3.tgz#0082b3ca9734679e1bd534995bdd3b24cf10f204" @@ -5504,6 +5797,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + lru-cache@~2.6.5: version "2.6.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.6.5.tgz#e56d6354148ede8d7707b58d143220fd08df0fd5" @@ -5515,9 +5815,12 @@ make-dir@^2.0.0, make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -mamacro@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" +make-dir@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" map-age-cleaner@^0.1.1: version "0.1.3" @@ -5596,13 +5899,21 @@ mem@^4.0.0: mimic-fn "^1.0.0" p-is-promise "^2.0.0" -memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: +memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" dependencies: errno "^0.1.3" readable-stream "^2.0.1" +memory-fs@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" + integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" @@ -5668,7 +5979,7 @@ micromatch@^2.3.11: parse-glob "^3.0.4" regex-cache "^0.4.2" -micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: +micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" dependencies: @@ -5788,6 +6099,27 @@ minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.2: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + minipass@^2.2.1, minipass@^2.3.4: version "2.3.5" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" @@ -5795,12 +6127,27 @@ minipass@^2.2.1, minipass@^2.3.4: safe-buffer "^5.1.2" yallist "^3.0.0" +minipass@^3.0.0, minipass@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + minizlib@^1.1.1: version "1.2.1" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" dependencies: minipass "^2.2.1" +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + mississippi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" @@ -5823,13 +6170,20 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" dependencies: minimist "0.0.8" -mkdirp@^1.0.4: +mkdirp@^0.5.3: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -5951,6 +6305,11 @@ neo-async@^2.5.0: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" +neo-async@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + netmask@~1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" @@ -5980,9 +6339,10 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-libs-browser@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.0.tgz#c72f60d9d46de08a940dedbb25f3ffa2f9bbaa77" +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== dependencies: assert "^1.1.1" browserify-zlib "^0.2.0" @@ -5994,7 +6354,7 @@ node-libs-browser@^2.0.0: events "^3.0.0" https-browserify "^1.0.0" os-browserify "^0.3.0" - path-browserify "0.0.0" + path-browserify "0.0.1" process "^0.11.10" punycode "^1.2.4" querystring-es3 "^0.2.0" @@ -6006,7 +6366,7 @@ node-libs-browser@^2.0.0: tty-browserify "0.0.0" url "^0.11.0" util "^0.11.0" - vm-browserify "0.0.4" + vm-browserify "^1.0.1" node-modules-regexp@^1.0.0: version "1.0.0" @@ -6085,7 +6445,7 @@ normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -6321,6 +6681,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -6486,9 +6853,10 @@ pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" -path-browserify@0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.0.tgz#a0b870729aae214005b7d5032ec2cbbb0fb4451a" +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== path-dirname@^1.0.0: version "1.0.2" @@ -6572,6 +6940,11 @@ phoenix@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/phoenix/-/phoenix-1.4.0.tgz#9cec8dbd8cbc59ecd2147bc09ca8ceb56b860d75" +picomatch@^2.0.4: + version "2.2.3" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.3.tgz#465547f359ccc206d3c48e46a1bcb89bf7ee619d" + integrity sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg== + picomatch@^2.0.5, picomatch@^2.2.1: version "2.2.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" @@ -6620,6 +6993,13 @@ pkg-dir@^3.0.0: dependencies: find-up "^3.0.0" +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + pngjs@^3.3.0: version "3.3.3" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.3.tgz#85173703bde3edac8998757b96e5821d0966a21b" @@ -7196,9 +7576,10 @@ randomatic@^3.0.0: kind-of "^6.0.0" math-random "^1.0.1" -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== dependencies: safe-buffer "^5.1.0" @@ -7335,6 +7716,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" + integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== + dependencies: + picomatch "^2.2.1" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -7630,12 +8018,19 @@ rfdc@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" -rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2: +rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" dependencies: glob "^7.1.3" +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -7650,6 +8045,11 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^3.0.0" inherits "^2.0.1" +ruffle-mirror@^2021.4.10: + version "2021.4.11" + resolved "https://registry.yarnpkg.com/ruffle-mirror/-/ruffle-mirror-2021.4.11.tgz#039940e0a68e6849259dbef6b54fb877ac4373e7" + integrity sha512-a3N2OkPCJauiHBloHoZgCn/mSUlybyb9Ps4ikPGgHUy8iXPy6qMqh62imvNDU07tBJc5Y0c5mRHBFJRgpMgEpA== + run-async@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" @@ -7725,6 +8125,15 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" +schema-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" + integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== + dependencies: + "@types/json-schema" "^7.0.6" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + selenium-server@2.53.1: version "2.53.1" resolved "https://registry.yarnpkg.com/selenium-server/-/selenium-server-2.53.1.tgz#d681528812f3c2e0531a6b7e613e23bb02cce8a6" @@ -7742,7 +8151,7 @@ semver@^5.5.1: version "5.7.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" -semver@^6.3.0: +semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -7769,9 +8178,19 @@ send@0.16.2: range-parser "~1.2.0" statuses "~1.4.0" -serialize-javascript@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4" + integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== + dependencies: + randombytes "^2.1.0" serve-static@1.13.2: version "1.13.2" @@ -7842,9 +8261,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" @@ -8010,9 +8430,10 @@ source-map-support@^0.5.16: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@~0.5.10: - version "0.5.12" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" +source-map-support@~0.5.12: + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -8078,6 +8499,13 @@ ssri@^6.0.1: dependencies: figgy-pudding "^3.5.1" +ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + state-toggle@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe" @@ -8422,7 +8850,7 @@ table@^5.4.6: slice-ansi "^2.1.0" string-width "^3.0.0" -tapable@^1.0.0, tapable@^1.1.0: +tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -8438,6 +8866,18 @@ tar@^4: safe-buffer "^5.1.2" yallist "^3.0.2" +tar@^6.0.2: + version "6.1.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" + integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + tcp-port-used@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tcp-port-used/-/tcp-port-used-1.0.1.tgz#46061078e2d38c73979a2c2c12b5a674e6689d70" @@ -8445,27 +8885,29 @@ tcp-port-used@^1.0.1: debug "4.1.0" is2 "2.0.1" -terser-webpack-plugin@^1.1.0: - version "1.2.4" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.4.tgz#56f87540c28dd5265753431009388f473b5abba3" +terser-webpack-plugin@^1.4.3: + version "1.4.5" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz#a217aefaea330e734ffacb6120ec1fa312d6040b" + integrity sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw== dependencies: - cacache "^11.3.2" - find-cache-dir "^2.0.0" + cacache "^12.0.2" + find-cache-dir "^2.1.0" is-wsl "^1.1.0" schema-utils "^1.0.0" - serialize-javascript "^1.7.0" + serialize-javascript "^4.0.0" source-map "^0.6.1" - terser "^3.17.0" - webpack-sources "^1.3.0" + terser "^4.1.2" + webpack-sources "^1.4.0" worker-farm "^1.7.0" -terser@^3.17.0: - version "3.17.0" - resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" +terser@^4.1.2: + version "4.8.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" + integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== dependencies: - commander "^2.19.0" + commander "^2.20.0" source-map "~0.6.1" - source-map-support "~0.5.10" + source-map-support "~0.5.12" text-encoding@0.6.4: version "0.6.4" @@ -8912,20 +9354,15 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" -vm-browserify@0.0.4: - version "0.0.4" - resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-0.0.4.tgz#5d7ea45bbef9e4a6ff65f95438e0a87c357d5a73" - dependencies: - indexof "0.0.1" +vm-browserify@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== 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" @@ -8999,13 +9436,23 @@ vuex@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.0.1.tgz#e761352ebe0af537d4bb755a9b9dc4be3df7efd2" -watchpack@^1.5.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" +watchpack-chokidar2@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957" + integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww== + dependencies: + chokidar "^2.1.8" + +watchpack@^1.7.4: + version "1.7.5" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453" + integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ== dependencies: - chokidar "^2.0.2" graceful-fs "^4.1.2" neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.1" + watchpack-chokidar2 "^2.0.1" webpack-dev-middleware@^3.2.0, webpack-dev-middleware@^3.6.0: version "3.7.0" @@ -9041,41 +9488,49 @@ webpack-merge@^0.14.1: lodash.isplainobject "^3.2.0" lodash.merge "^3.3.2" -webpack-sources@^1.1.0, webpack-sources@^1.3.0: +webpack-sources@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" dependencies: source-list-map "^2.0.0" source-map "~0.6.1" -webpack@^4.0.0: - version "4.32.1" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.32.1.tgz#afe0cc7dd2b196e5a58f8d1d385311cfbb5d68c0" +webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== dependencies: - "@webassemblyjs/ast" "1.8.5" - "@webassemblyjs/helper-module-context" "1.8.5" - "@webassemblyjs/wasm-edit" "1.8.5" - "@webassemblyjs/wasm-parser" "1.8.5" - acorn "^6.0.5" - acorn-dynamic-import "^4.0.0" - ajv "^6.1.0" - ajv-keywords "^3.1.0" - chrome-trace-event "^1.0.0" - enhanced-resolve "^4.1.0" - eslint-scope "^4.0.0" + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack@^4.44.0: + version "4.46.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.46.0.tgz#bf9b4404ea20a073605e0a011d188d77cb6ad542" + integrity sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q== + dependencies: + "@webassemblyjs/ast" "1.9.0" + "@webassemblyjs/helper-module-context" "1.9.0" + "@webassemblyjs/wasm-edit" "1.9.0" + "@webassemblyjs/wasm-parser" "1.9.0" + acorn "^6.4.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.5.0" + eslint-scope "^4.0.3" json-parse-better-errors "^1.0.2" - loader-runner "^2.3.0" - loader-utils "^1.1.0" - memory-fs "~0.4.1" - micromatch "^3.1.8" - mkdirp "~0.5.0" - neo-async "^2.5.0" - node-libs-browser "^2.0.0" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.3" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" schema-utils "^1.0.0" - tapable "^1.1.0" - terser-webpack-plugin "^1.1.0" - watchpack "^1.5.0" - webpack-sources "^1.3.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.3" + watchpack "^1.7.4" + webpack-sources "^1.4.1" whet.extend@~0.9.9: version "0.9.9" @@ -9179,6 +9634,11 @@ yallist@^3.0.0, yallist@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yaml@^1.7.2: version "1.10.0" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" @@ -9227,3 +9687,8 @@ yauzl@^2.10.0: yeast@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==