Merge remote-tracking branch 'upstream/develop' into shigusegubu

* upstream/develop:
  Feature/polls attempt 2
  Update translation with review comments
  Update file with current en.json
  Update fr.json added missing ,
  Update fr.json stoped at line 134, more to do below
This commit is contained in:
Henry Jameson 2019-06-18 23:45:28 +03:00
commit 9f8b0ce5e1
57 changed files with 1909 additions and 1663 deletions

View file

@ -35,7 +35,6 @@
"vue-popperjs": "^2.0.3",
"vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4",
"vue-timeago": "^3.1.2",
"vuelidate": "^0.7.4",
"vuex": "^3.0.1",
"whatwg-fetch": "^2.0.3"

View file

@ -184,7 +184,43 @@ input, textarea, .select {
flex: 1;
}
&[type=radio],
&[type=radio] {
display: none;
&:checked + label::before {
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
background-color: var(--link, $fallback--link);
}
&:disabled {
&,
& + label,
& + label::before {
opacity: .5;
}
}
+ label::before {
display: inline-block;
content: '';
transition: box-shadow 200ms;
width: 1.1em;
height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle
box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow);
margin-right: .5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top;
text-align: center;
line-height: 1.1em;
font-size: 1.1em;
box-sizing: border-box;
color: transparent;
overflow: hidden;
box-sizing: border-box;
}
}
&[type=checkbox] {
display: none;
&:checked + label::before {
@ -230,6 +266,15 @@ option {
background-color: var(--bg, $fallback--bg);
}
.hide-number-spinner {
-moz-appearance: textfield;
&[type=number]::-webkit-inner-spin-button,
&[type=number]::-webkit-outer-spin-button {
opacity: 0;
display: none;
}
}
i[class*=icon-] {
color: $fallback--icon;
color: var(--icon, $fallback--icon)

View file

@ -215,11 +215,12 @@ const getNodeInfo = async ({ store }) => {
if (res.ok) {
const data = await res.json()
const metadata = data.metadata
const features = metadata.features
store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') })
store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') })
store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') })
store.dispatch('setInstanceOption', { name: 'pollsAvailable', value: features.includes('polls') })
store.dispatch('setInstanceOption', { name: 'pollLimits', value: metadata.pollLimits })
store.dispatch('setInstanceOption', { name: 'restrictedNicknames', value: metadata.restrictedNicknames })
store.dispatch('setInstanceOption', { name: 'postFormats', value: metadata.postFormats })

View file

@ -13,7 +13,7 @@
<style>
.media-upload {
font-size: 26px;
flex: 1;
min-width: 50px;
}
.icon-upload {

View file

@ -1,6 +1,7 @@
import Status from '../status/status.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -13,7 +14,10 @@ const Notification = {
},
props: [ 'notification' ],
components: {
Status, UserAvatar, UserCard
Status,
UserAvatar,
UserCard,
Timeago
},
methods: {
toggleUserExpanded () {

View file

@ -30,12 +30,12 @@
</div>
<div class="timeago" v-if="notification.type === 'follow'">
<span class="faint">
<timeago :since="notification.created_at" :auto-update="240"></timeago>
<Timeago :time="notification.created_at" :auto-update="240"></Timeago>
</span>
</div>
<div class="timeago" v-else>
<router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" class="faint-link">
<timeago :since="notification.created_at" :auto-update="240"></timeago>
<Timeago :time="notification.created_at" :auto-update="240"></Timeago>
</router-link>
</div>
</span>

107
src/components/poll/poll.js Normal file
View file

@ -0,0 +1,107 @@
import Timeago from '../timeago/timeago.vue'
import { forEach, map } from 'lodash'
export default {
name: 'Poll',
props: ['poll', 'statusId'],
components: { Timeago },
data () {
return {
loading: false,
choices: [],
refreshInterval: null
}
},
created () {
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000)
// Initialize choices to booleans and set its length to match options
this.choices = this.poll.options.map(_ => false)
},
destroyed () {
clearTimeout(this.refreshInterval)
},
computed: {
expired () {
return Date.now() > Date.parse(this.poll.expires_at)
},
loggedIn () {
return this.$store.state.users.currentUser
},
showResults () {
return this.poll.voted || this.expired || !this.loggedIn
},
totalVotesCount () {
return this.poll.votes_count
},
expiresAt () {
return Date.parse(this.poll.expires_at).toLocaleString()
},
containerClass () {
return {
loading: this.loading
}
},
choiceIndices () {
// Convert array of booleans into an array of indices of the
// items that were 'true', so [true, false, false, true] becomes
// [0, 3].
return this.choices
.map((entry, index) => entry && index)
.filter(value => typeof value === 'number')
},
isDisabled () {
const noChoice = this.choiceIndices.length === 0
return this.loading || noChoice
}
},
methods: {
refreshPoll () {
if (this.expired) return
this.fetchPoll()
this.refreshInterval = setTimeout(this.refreshPoll, 30 * 1000)
},
percentageForOption (count) {
return this.totalVotesCount === 0 ? 0 : Math.round(count / this.totalVotesCount * 100)
},
resultTitle (option) {
return `${option.votes_count}/${this.totalVotesCount} ${this.$t('polls.votes')}`
},
fetchPoll () {
this.$store.dispatch('refreshPoll', { id: this.statusId, pollId: this.poll.id })
},
activateOption (index) {
// forgive me father: doing checking the radio/checkboxes
// in code because of customized input elements need either
// a) an extra element for the actual graphic, or b) use a
// pseudo element for the label. We use b) which mandates
// using "for" and "id" matching which isn't nice when the
// same poll appears multiple times on the site (notifs and
// timeline for example). With code we can make sure it just
// works without altering the pseudo element implementation.
const allElements = this.$el.querySelectorAll('input')
const clickedElement = this.$el.querySelector(`input[value="${index}"]`)
if (this.poll.multiple) {
// Checkboxes, toggle only the clicked one
clickedElement.checked = !clickedElement.checked
} else {
// Radio button, uncheck everything and check the clicked one
forEach(allElements, element => { element.checked = false })
clickedElement.checked = true
}
this.choices = map(allElements, e => e.checked)
},
optionId (index) {
return `poll${this.poll.id}-${index}`
},
vote () {
if (this.choiceIndices.length === 0) return
this.loading = true
this.$store.dispatch(
'votePoll',
{ id: this.statusId, pollId: this.poll.id, choices: this.choiceIndices }
).then(poll => {
this.loading = false
})
}
}
}

View file

@ -0,0 +1,117 @@
<template>
<div class="poll" v-bind:class="containerClass">
<div
class="poll-option"
v-for="(option, index) in poll.options"
:key="index"
>
<div v-if="showResults" :title="resultTitle(option)" class="option-result">
<div class="option-result-label">
<span class="result-percentage">
{{percentageForOption(option.votes_count)}}%
</span>
<span>{{option.title}}</span>
</div>
<div
class="result-fill"
:style="{ 'width': `${percentageForOption(option.votes_count)}%` }"
>
</div>
</div>
<div v-else @click="activateOption(index)">
<input
v-if="poll.multiple"
type="checkbox"
:disabled="loading"
:value="index"
>
<input
v-else
type="radio"
:disabled="loading"
:value="index"
>
<label>
{{option.title}}
</label>
</div>
</div>
<div class="footer faint">
<button
v-if="!showResults"
class="btn btn-default poll-vote-button"
type="button"
@click="vote"
:disabled="isDisabled"
>
{{$t('polls.vote')}}
</button>
<div class="total">
{{totalVotesCount}} {{ $t("polls.votes") }}&nbsp;·&nbsp;
</div>
<i18n :path="expired ? 'polls.expired' : 'polls.expires_in'">
<Timeago :time="this.poll.expires_at" :auto-update="60" :now-threshold="0" />
</i18n>
</div>
</div>
</template>
<script src="./poll.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.poll {
.votes {
display: flex;
flex-direction: column;
margin: 0 0 0.5em;
}
.poll-option {
margin: 0.5em 0;
height: 1.5em;
}
.option-result {
height: 100%;
display: flex;
flex-direction: row;
position: relative;
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
.option-result-label {
display: flex;
align-items: center;
padding: 0.1em 0.25em;
z-index: 1;
}
.result-percentage {
width: 3.5em;
}
.result-fill {
height: 100%;
position: absolute;
background-color: $fallback--lightBg;
background-color: var(--linkBg, $fallback--lightBg);
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
top: 0;
left: 0;
transition: width 0.5s;
}
input {
width: 3.5em;
}
.footer {
display: flex;
align-items: center;
}
&.loading * {
cursor: progress;
}
.poll-vote-button {
padding: 0 0.5em;
margin-right: 0.5em;
}
}
</style>

View file

@ -0,0 +1,121 @@
import * as DateUtils from 'src/services/date_utils/date_utils.js'
import { uniq } from 'lodash'
export default {
name: 'PollForm',
props: ['visible'],
data: () => ({
pollType: 'single',
options: ['', ''],
expiryAmount: 10,
expiryUnit: 'minutes'
}),
computed: {
pollLimits () {
return this.$store.state.instance.pollLimits
},
maxOptions () {
return this.pollLimits.max_options
},
maxLength () {
return this.pollLimits.max_option_chars
},
expiryUnits () {
const allUnits = ['minutes', 'hours', 'days']
const expiry = this.convertExpiryFromUnit
return allUnits.filter(
unit => this.pollLimits.max_expiration >= expiry(unit, 1)
)
},
minExpirationInCurrentUnit () {
return Math.ceil(
this.convertExpiryToUnit(
this.expiryUnit,
this.pollLimits.min_expiration
)
)
},
maxExpirationInCurrentUnit () {
return Math.floor(
this.convertExpiryToUnit(
this.expiryUnit,
this.pollLimits.max_expiration
)
)
}
},
methods: {
clear () {
this.pollType = 'single'
this.options = ['', '']
this.expiryAmount = 10
this.expiryUnit = 'minutes'
},
nextOption (index) {
const element = this.$el.querySelector(`#poll-${index + 1}`)
if (element) {
element.focus()
} else {
// Try adding an option and try focusing on it
const addedOption = this.addOption()
if (addedOption) {
this.$nextTick(function () {
this.nextOption(index)
})
}
}
},
addOption () {
if (this.options.length < this.maxOptions) {
this.options.push('')
return true
}
return false
},
deleteOption (index, event) {
if (this.options.length > 2) {
this.options.splice(index, 1)
}
},
convertExpiryToUnit (unit, amount) {
// Note: we want seconds and not milliseconds
switch (unit) {
case 'minutes': return (1000 * amount) / DateUtils.MINUTE
case 'hours': return (1000 * amount) / DateUtils.HOUR
case 'days': return (1000 * amount) / DateUtils.DAY
}
},
convertExpiryFromUnit (unit, amount) {
// Note: we want seconds and not milliseconds
switch (unit) {
case 'minutes': return 0.001 * amount * DateUtils.MINUTE
case 'hours': return 0.001 * amount * DateUtils.HOUR
case 'days': return 0.001 * amount * DateUtils.DAY
}
},
expiryAmountChange () {
this.expiryAmount =
Math.max(this.minExpirationInCurrentUnit, this.expiryAmount)
this.expiryAmount =
Math.min(this.maxExpirationInCurrentUnit, this.expiryAmount)
this.updatePollToParent()
},
updatePollToParent () {
const expiresIn = this.convertExpiryFromUnit(
this.expiryUnit,
this.expiryAmount
)
const options = uniq(this.options.filter(option => option !== ''))
if (options.length < 2) {
this.$emit('update-poll', { error: this.$t('polls.not_enough_options') })
return
}
this.$emit('update-poll', {
options,
multiple: this.pollType === 'multiple',
expiresIn
})
}
}
}

View file

@ -0,0 +1,133 @@
<template>
<div class="poll-form" v-if="visible">
<div class="poll-option" v-for="(option, index) in options" :key="index">
<div class="input-container">
<input
class="poll-option-input"
type="text"
:placeholder="$t('polls.option')"
:maxlength="maxLength"
:id="`poll-${index}`"
v-model="options[index]"
@change="updatePollToParent"
@keydown.enter.stop.prevent="nextOption(index)"
>
</div>
<div class="icon-container" v-if="options.length > 2">
<i class="icon-cancel" @click="deleteOption(index)"></i>
</div>
</div>
<a
v-if="options.length < maxOptions"
class="add-option faint"
@click="addOption"
>
<i class="icon-plus" />
{{ $t("polls.add_option") }}
</a>
<div class="poll-type-expiry">
<div class="poll-type" :title="$t('polls.type')">
<label for="poll-type-selector" class="select">
<select class="select" v-model="pollType" @change="updatePollToParent">
<option value="single">{{$t('polls.single_choice')}}</option>
<option value="multiple">{{$t('polls.multiple_choices')}}</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
<div class="poll-expiry" :title="$t('polls.expiry')">
<input
type="number"
class="expiry-amount hide-number-spinner"
:min="minExpirationInCurrentUnit"
:max="maxExpirationInCurrentUnit"
v-model="expiryAmount"
@change="expiryAmountChange"
>
<label class="expiry-unit select">
<select
v-model="expiryUnit"
@change="expiryAmountChange"
>
<option v-for="unit in expiryUnits" :value="unit">
{{ $t(`time.${unit}_short`, ['']) }}
</option>
</select>
<i class="icon-down-open"/>
</label>
</div>
</div>
</div>
</template>
<script src="./poll_form.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.poll-form {
display: flex;
flex-direction: column;
padding: 0 0.5em 0.5em;
.add-option {
align-self: flex-start;
padding-top: 0.25em;
cursor: pointer;
}
.poll-option {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 0.25em;
}
.input-container {
width: 100%;
input {
// Hack: dodge the floating X icon
padding-right: 2.5em;
width: 100%;
}
}
.icon-container {
// Hack: Move the icon over the input box
width: 2em;
margin-left: -2em;
z-index: 1;
}
.poll-type-expiry {
margin-top: 0.5em;
display: flex;
width: 100%;
}
.poll-type {
margin-right: 0.75em;
flex: 1 1 60%;
.select {
border: none;
box-shadow: none;
background-color: transparent;
}
}
.poll-expiry {
display: flex;
.expiry-amount {
width: 3em;
text-align: right;
}
.expiry-unit {
border: none;
box-shadow: none;
background-color: transparent;
}
}
}
</style>

View file

@ -2,6 +2,7 @@ import statusPoster from '../../services/status_poster/status_poster.service.js'
import MediaUpload from '../media_upload/media_upload.vue'
import ScopeSelector from '../scope_selector/scope_selector.vue'
import EmojiInput from '../emoji-input/emoji-input.vue'
import PollForm from '../poll/poll_form.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { reject, map, uniqBy } from 'lodash'
import suggestor from '../emoji-input/suggestor.js'
@ -31,8 +32,9 @@ const PostStatusForm = {
],
components: {
MediaUpload,
ScopeSelector,
EmojiInput
EmojiInput,
PollForm,
ScopeSelector
},
mounted () {
this.resize(this.$refs.textarea)
@ -75,10 +77,12 @@ const PostStatusForm = {
status: statusText,
nsfw: false,
files: [],
poll: {},
visibility: scope,
contentType
},
caret: 0
caret: 0,
pollFormVisible: false
}
},
computed: {
@ -153,8 +157,17 @@ const PostStatusForm = {
safeDMEnabled () {
return this.$store.state.instance.safeDM
},
pollsAvailable () {
return this.$store.state.instance.pollsAvailable &&
this.$store.state.instance.pollLimits.max_options >= 2
},
hideScopeNotice () {
return this.$store.state.config.hideScopeNotice
},
pollContentError () {
return this.pollFormVisible &&
this.newStatus.poll &&
this.newStatus.poll.error
}
},
methods: {
@ -171,6 +184,12 @@ const PostStatusForm = {
}
}
const poll = this.pollFormVisible ? this.newStatus.poll : {}
if (this.pollContentError) {
this.error = this.pollContentError
return
}
this.posting = true
statusPoster.postStatus({
status: newStatus.status,
@ -180,7 +199,8 @@ const PostStatusForm = {
media: newStatus.files,
store: this.$store,
inReplyToStatusId: this.replyTo,
contentType: newStatus.contentType
contentType: newStatus.contentType,
poll
}).then((data) => {
if (!data.error) {
this.newStatus = {
@ -188,9 +208,12 @@ const PostStatusForm = {
spoilerText: '',
files: [],
visibility: newStatus.visibility,
contentType: newStatus.contentType
contentType: newStatus.contentType,
poll: {}
}
this.pollFormVisible = false
this.$refs.mediaUpload.clearFile()
this.clearPollForm()
this.$emit('posted')
let el = this.$el.querySelector('textarea')
el.style.height = 'auto'
@ -261,6 +284,17 @@ const PostStatusForm = {
changeVis (visibility) {
this.newStatus.visibility = visibility
},
togglePollForm () {
this.pollFormVisible = !this.pollFormVisible
},
setPoll (poll) {
this.newStatus.poll = poll
},
clearPollForm () {
if (this.$refs.pollForm) {
this.$refs.pollForm.clear()
}
},
dismissScopeNotice () {
this.$store.dispatch('setOption', { name: 'hideScopeNotice', value: true })
}

View file

@ -1,6 +1,6 @@
<template>
<div class="post-status-form">
<form @submit.prevent="postStatus(newStatus)">
<form @submit.prevent="postStatus(newStatus)" autocomplete="off">
<div class="form-group" >
<i18n
v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private'"
@ -91,9 +91,24 @@
:onScopeChange="changeVis"/>
</div>
</div>
<poll-form
ref="pollForm"
v-if="pollsAvailable"
:visible="pollFormVisible"
@update-poll="setPoll"
/>
<div class='form-bottom'>
<div class='form-bottom-left'>
<media-upload ref="mediaUpload" @uploading="disableSubmit" @uploaded="addMediaFile" @upload-failed="uploadFailed" :drop-files="dropFiles"></media-upload>
<div v-if="pollsAvailable" class="poll-icon">
<i
:title="$t('polls.add_poll')"
@click="togglePollForm"
class="icon-chart-bar btn btn-default"
:class="pollFormVisible && 'selected'"
/>
</div>
</div>
<p v-if="isOverLengthLimit" class="error">{{ charactersLeft }}</p>
<p class="faint" v-else-if="hasStatusLengthLimit">{{ charactersLeft }}</p>
@ -172,6 +187,11 @@
}
}
.form-bottom-left {
display: flex;
flex: 1;
}
.text-format {
.only-format {
color: $fallback--faint;
@ -179,6 +199,20 @@
}
}
.poll-icon {
font-size: 26px;
flex: 1;
.selected {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}
}
.icon-chart-bar {
cursor: pointer;
}
.error {
text-align: center;
@ -240,7 +274,6 @@
}
}
.btn {
cursor: pointer;
}

View file

@ -1,6 +1,7 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
import RetweetButton from '../retweet_button/retweet_button.vue'
import Poll from '../poll/poll.vue'
import ExtraButtons from '../extra_buttons/extra_buttons.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCard from '../user_card/user_card.vue'
@ -8,6 +9,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.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 { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
@ -285,11 +287,13 @@ const Status = {
RetweetButton,
ExtraButtons,
PostStatusForm,
Poll,
UserCard,
UserAvatar,
Gallery,
LinkPreview,
AvatarList
AvatarList,
Timeago
},
methods: {
visibilityIcon (visibility) {

View file

@ -52,7 +52,7 @@
<span class="heading-right">
<router-link class="timeago faint-link" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
<Timeago :time="status.created_at" :auto-update="60"></Timeago>
</router-link>
<div class="button-icon visibility-icon" v-if="status.visibility">
<i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
@ -123,6 +123,10 @@
<a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">{{$t("general.show_less")}}</a>
</div>
<div v-if="status.poll && status.poll.options">
<poll :poll="status.poll" :status-id="status.id" />
</div>
<div v-if="status.attachments && (!hideSubjectStatus || showingLongSubject)" class="attachments media-body">
<attachment
class="non-gallery"

View file

@ -0,0 +1,48 @@
<template>
<time :datetime="time" :title="localeDateString">
{{ $t(relativeTime.key, [relativeTime.num]) }}
</time>
</template>
<script>
import * as DateUtils from 'src/services/date_utils/date_utils.js'
export default {
name: 'Timeago',
props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold'],
data () {
return {
relativeTime: { key: 'time.now', num: 0 },
interval: null
}
},
created () {
this.refreshRelativeTimeObject()
},
destroyed () {
clearTimeout(this.interval)
},
computed: {
localeDateString () {
return typeof this.time === 'string'
? new Date(Date.parse(this.time)).toLocaleString()
: this.time.toLocaleString()
}
},
methods: {
refreshRelativeTimeObject () {
const nowThreshold = typeof this.nowThreshold === 'number' ? this.nowThreshold : 1
this.relativeTime = this.longFormat
? DateUtils.relativeTime(this.time, nowThreshold)
: DateUtils.relativeTimeShort(this.time, nowThreshold)
if (this.autoUpdate) {
this.interval = setTimeout(
this.refreshRelativeTimeObject,
1000 * this.autoUpdate
)
}
}
}
}
</script>

View file

@ -168,6 +168,40 @@
"true": "sí"
}
},
"time": {
"day": "{0} dia",
"days": "{0} dies",
"day_short": "{0} dia",
"days_short": "{0} dies",
"hour": "{0} hour",
"hours": "{0} hours",
"hour_short": "{0}h",
"hours_short": "{0}h",
"in_future": "in {0}",
"in_past": "fa {0}",
"minute": "{0} minute",
"minutes": "{0} minutes",
"minute_short": "{0}min",
"minutes_short": "{0}min",
"month": "{0} mes",
"months": "{0} mesos",
"month_short": "{0} mes",
"months_short": "{0} mesos",
"now": "ara mateix",
"now_short": "ara mateix",
"second": "{0} second",
"seconds": "{0} seconds",
"second_short": "{0}s",
"seconds_short": "{0}s",
"week": "{0} setm.",
"weeks": "{0} setm.",
"week_short": "{0} setm.",
"weeks_short": "{0} setm.",
"year": "{0} any",
"years": "{0} anys",
"year_short": "{0} any",
"years_short": "{0} anys"
},
"timeline": {
"collapse": "Replega",
"conversation": "Conversa",

View file

@ -350,6 +350,40 @@
}
}
},
"time": {
"day": "{0} day",
"days": "{0} days",
"day_short": "{0}d",
"days_short": "{0}d",
"hour": "{0} hour",
"hours": "{0} hours",
"hour_short": "{0}h",
"hours_short": "{0}h",
"in_future": "in {0}",
"in_past": "{0} ago",
"minute": "{0} minute",
"minutes": "{0} minutes",
"minute_short": "{0}min",
"minutes_short": "{0}min",
"month": "{0} měs",
"months": "{0} měs",
"month_short": "{0} měs",
"months_short": "{0} měs",
"now": "teď",
"now_short": "teď",
"second": "{0} second",
"seconds": "{0} seconds",
"second_short": "{0}s",
"seconds_short": "{0}s",
"week": "{0} týd",
"weeks": "{0} týd",
"week_short": "{0} týd",
"weeks_short": "{0} týd",
"year": "{0} r",
"years": "{0} l",
"year_short": "{0}r",
"years_short": "{0}l"
},
"timeline": {
"collapse": "Zabalit",
"conversation": "Konverzace",

View file

@ -91,6 +91,20 @@
"repeated_you": "repeated your status",
"no_more_notifications": "No more notifications"
},
"polls": {
"add_poll": "Add Poll",
"add_option": "Add Option",
"option": "Option",
"votes": "votes",
"vote": "Vote",
"type": "Poll type",
"single_choice": "Single choice",
"multiple_choices": "Multiple choices",
"expiry": "Poll age",
"expires_in": "Poll ends in {0}",
"expired": "Poll ended {0} ago",
"not_enough_options": "Too few unique options in poll"
},
"interactions": {
"favs_repeats": "Repeats and Favorites",
"follows": "New follows",
@ -435,6 +449,40 @@
"frontend_version": "Frontend Version"
}
},
"time": {
"day": "{0} day",
"days": "{0} days",
"day_short": "{0}d",
"days_short": "{0}d",
"hour": "{0} hour",
"hours": "{0} hours",
"hour_short": "{0}h",
"hours_short": "{0}h",
"in_future": "in {0}",
"in_past": "{0} ago",
"minute": "{0} minute",
"minutes": "{0} minutes",
"minute_short": "{0}min",
"minutes_short": "{0}min",
"month": "{0} month",
"months": "{0} months",
"month_short": "{0}mo",
"months_short": "{0}mo",
"now": "just now",
"now_short": "now",
"second": "{0} second",
"seconds": "{0} seconds",
"second_short": "{0}s",
"seconds_short": "{0}s",
"week": "{0} week",
"weeks": "{0} weeks",
"week_short": "{0}w",
"weeks_short": "{0}w",
"year": "{0} year",
"years": "{0} years",
"year_short": "{0}y",
"years_short": "{0}y"
},
"timeline": {
"collapse": "Collapse",
"conversation": "Conversation",

View file

@ -36,6 +36,7 @@
"chat": "Paikallinen Chat",
"friend_requests": "Seurauspyynnöt",
"mentions": "Maininnat",
"interactions": "Interaktiot",
"dms": "Yksityisviestit",
"public_tl": "Julkinen Aikajana",
"timeline": "Aikajana",
@ -54,6 +55,25 @@
"repeated_you": "toisti viestisi",
"no_more_notifications": "Ei enempää ilmoituksia"
},
"polls": {
"add_poll": "Lisää äänestys",
"add_option": "Lisää vaihtoehto",
"option": "Vaihtoehto",
"votes": "ääntä",
"vote": "Äänestä",
"type": "Äänestyksen tyyppi",
"single_choice": "Yksi valinta",
"multiple_choices": "Monivalinta",
"expiry": "Äänestyksen kesto",
"expires_in": "Päättyy {0} päästä",
"expired": "Päättyi {0} sitten",
"not_enough_option": "Liian vähän uniikkeja vaihtoehtoja äänestyksessä"
},
"interactions": {
"favs_repeats": "Toistot ja tykkäykset",
"follows": "Uudet seuraukset",
"load_older": "Lataa vanhempia interaktioita"
},
"post_status": {
"new_status": "Uusi viesti",
"account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi",
@ -210,6 +230,40 @@
"true": "päällä"
}
},
"time": {
"day": "{0} päivä",
"days": "{0} päivää",
"day_short": "{0}pv",
"days_short": "{0}pv",
"hour": "{0} tunti",
"hours": "{0} tuntia",
"hour_short": "{0}t",
"hours_short": "{0}t",
"in_future": "{0} tulevaisuudessa",
"in_past": "{0} sitten",
"minute": "{0} minuutti",
"minutes": "{0} minuuttia",
"minute_short": "{0}min",
"minutes_short": "{0}min",
"month": "{0} kuukausi",
"months": "{0} kuukautta",
"month_short": "{0}kk",
"months_short": "{0}kk",
"now": "nyt",
"now_short": "juuri nyt",
"second": "{0} sekunti",
"seconds": "{0} sekuntia",
"second_short": "{0}s",
"seconds_short": "{0}s",
"week": "{0} viikko",
"weeks": "{0} viikkoa",
"week_short": "{0}vk",
"weeks_short": "{0}vk",
"year": "{0} vuosi",
"years": "{0} vuotta",
"year_short": "{0}v",
"years_short": "{0}v"
},
"timeline": {
"collapse": "Sulje",
"conversation": "Keskustelu",

View file

@ -2,22 +2,47 @@
"chat": {
"title": "Chat"
},
"exporter": {
"export": "Exporter",
"processing": "En cours de traitement, vous pourrez bientôt télécharger votre fichier"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Proxy média",
"scope_options": "Options de visibilité",
"text_limit": "Limite du texte",
"text_limit": "Limite de texte",
"title": "Caractéristiques",
"who_to_follow": "Qui s'abonner"
"who_to_follow": "Personnes à suivre"
},
"finder": {
"error_fetching_user": "Erreur lors de la recherche de l'utilisateur",
"find_user": "Chercher un utilisateur"
"error_fetching_user": "Erreur lors de la recherche de l'utilisateur·ice",
"find_user": "Chercher un-e utilisateur·ice"
},
"general": {
"apply": "Appliquer",
"submit": "Envoyer"
"submit": "Envoyer",
"more": "Plus",
"generic_error": "Une erreur s'est produite",
"optional": "optionnel",
"show_more": "Montrer plus",
"show_less": "Montrer moins",
"cancel": "Annuler",
"disable": "Désactiver",
"enable": "Activer",
"confirm": "Confirmer",
"verify": "Vérifier"
},
"image_cropper": {
"crop_picture": "Rogner l'image",
"save": "Sauvegarder",
"save_without_cropping": "Sauvegarder sans rogner",
"cancel": "Annuler"
},
"importer": {
"submit": "Soumettre",
"success": "Importé avec succès.",
"error": "Une erreur est survenue pendant l'import de ce fichier."
},
"login": {
"login": "Connexion",
@ -26,37 +51,72 @@
"password": "Mot de passe",
"placeholder": "p.e. lain",
"register": "S'inscrire",
"username": "Identifiant"
"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",
"recovery_code": "Code de récupération",
"heading": {
"totp": "Authentification à double authentification",
"recovery": "Récuperation de la double authentification"
}
},
"media_modal": {
"previous": "Précédent",
"next": "Suivant"
},
"nav": {
"about": "À propos",
"back": "Retour",
"chat": "Chat local",
"friend_requests": "Demandes d'ami",
"dms": "Messages adressés",
"friend_requests": "Demandes de suivi",
"mentions": "Notifications",
"public_tl": "Statuts locaux",
"timeline": "Journal",
"twkn": "Le réseau connu"
"interactions": "Interactions",
"dms": "Messages directs",
"public_tl": "Fil d'actualité public",
"timeline": "Fil d'actualité",
"twkn": "Ensemble du réseau connu",
"user_search": "Recherche d'utilisateur·ice",
"who_to_follow": "Qui suivre",
"preferences": "Préférences"
},
"notifications": {
"broken_favorite": "Chargement d'un message inconnu ...",
"broken_favorite": "Chargement d'un message inconnu",
"favorited_you": "a aimé votre statut",
"followed_you": "a commencé à vous suivre",
"load_older": "Charger les notifications précédentes",
"notifications": "Notifications",
"read": "Lu !",
"repeated_you": "a partagé votre statut"
"read": "Lu !",
"repeated_you": "a partagé votre statut",
"no_more_notifications": "Aucune notification supplémentaire"
},
"interactions": {
"favs_repeats": "Partages et favoris",
"follows": "Nouveaux⋅elles abonné⋅e⋅s ?",
"load_older": "Chargez d'anciennes interactions"
},
"post_status": {
"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",
"content_type": {
"text/plain": "Texte brut"
"text/plain": "Texte brut",
"text/html": "HTML",
"text/markdown": "Markdown",
"text/bbcode": "BBCode"
},
"content_warning": "Sujet (optionnel)",
"default": "Écrivez ici votre prochain statut.",
"direct_warning": "Ce message sera visible à toutes les personnes mentionnées.",
"direct_warning_to_all": "Ce message sera visible pour toutes les personnes mentionnées.",
"direct_warning_to_first_only": "Ce message sera visible uniquement pour personnes mentionnées au début du message.",
"posting": "Envoi en cours",
"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"
},
"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",
@ -66,13 +126,53 @@
},
"registration": {
"bio": "Biographie",
"email": "Adresse email",
"email": "Adresse mail",
"fullname": "Pseudonyme",
"password_confirm": "Confirmation du mot de passe",
"registration": "Inscription",
"token": "Jeton d'invitation"
"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.",
"validations": {
"username_required": "ne peut pas être laissé vide",
"fullname_required": "ne peut pas être laissé vide",
"email_required": "ne peut pas être laissé vide",
"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"
}
},
"selectable_list": {
"select_all": "Tout selectionner"
},
"settings": {
"app_name": "Nom de l'application",
"security": "Sécurité",
"enter_current_password_to_confirm": "Entrez votre mot de passe actuel pour confirmer votre identité",
"mfa": {
"otp": "OTP",
"setup_otp": "Configurer OTP",
"wait_pre_setup_otp": "préconfiguration OTP",
"confirm_and_enable": "Confirmer & activer OTP",
"title": "Double authentification",
"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.",
"recovery_codes": "Codes de récupération.",
"waiting_a_recovery_codes": "Récéption 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",
"scan": {
"title": "Scanner",
"desc": "En utilisant votre application de double authentification, 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:"
}
},
"attachmentRadius": "Pièces jointes",
"attachments": "Pièces jointes",
"autoload": "Charger la suite automatiquement une fois le bas de la page atteint",
@ -81,31 +181,38 @@
"avatarRadius": "Avatars",
"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_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_tab": "Bloqué·e·s",
"btnRadius": "Boutons",
"cBlue": "Bleu (Répondre, suivre)",
"cGreen": "Vert (Partager)",
"cOrange": "Orange (Aimer)",
"cRed": "Rouge (Annuler)",
"cBlue": "Bleu (répondre, suivre)",
"cGreen": "Vert (partager)",
"cOrange": "Orange (aimer)",
"cRed": "Rouge (annuler)",
"change_password": "Changez votre mot de passe",
"change_password_error": "Il y a eu un problème pour changer votre mot de passe.",
"changed_password": "Mot de passe modifié avec succès !",
"changed_password": "Mot de passe modifié avec succès !",
"collapse_subject": "Réduire les messages avec des sujets",
"composing": "Composition",
"confirm_new_password": "Confirmation du nouveau mot de passe",
"current_avatar": "Avatar actuel",
"current_password": "Mot de passe actuel",
"current_profile_banner": "Bannière de profil actuelle",
"data_import_export_tab": "Import / Export des Données",
"default_vis": "Portée de visibilité par défaut",
"default_vis": "Visibilité par défaut",
"delete_account": "Supprimer le compte",
"delete_account_description": "Supprimer définitivement votre compte et tous vos statuts.",
"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 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'administrateur⋅ice 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_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_export_processing": "Exportation en cours…",
"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.",
@ -113,13 +220,22 @@
"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",
"max_thumbnails": "Nombre maximum de miniatures par statuts",
"hide_isp": "Masquer le panneau spécifique a 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)",
"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_theme": "Charger le thème",
"inputRadius": "Champs de texte",
"checkboxRadius": "Cases à cocher",
"instance_default": "(default: {value})",
"instance_default_simple": "(default)",
"interface": "Interface",
"interfaceLanguage": "Langue de l'interface",
"invalid_theme_imported": "Le fichier sélectionné n'est pas un thème Pleroma pris en charge. Aucun changement n'a été apporté à votre thème.",
"limited_availability": "Non disponible dans votre navigateur",
@ -127,15 +243,24 @@
"lock_account_description": "Limitez votre compte aux abonnés acceptés uniquement",
"loop_video": "Vidéos en boucle",
"loop_video_silent_only": "Boucle uniquement les vidéos sans le son (les « gifs » de Mastodon)",
"mutes_tab": "Comptes silenciés",
"play_videos_in_modal": "Jouer les vidéos directement dans le visionneur de médias",
"use_contain_fit": "Ne pas rogner les miniatures des pièces-jointes",
"name": "Nom",
"name_bio": "Nom & Bio",
"new_password": "Nouveau mot de passe",
"no_rich_text_description": "Ne formatez pas le texte",
"notification_visibility": "Types de notifications à afficher",
"notification_visibility_follows": "Abonnements",
"notification_visibility_likes": "Jaime",
"notification_visibility_likes": "J'aime",
"notification_visibility_mentions": "Mentionnés",
"notification_visibility_repeats": "Partages",
"no_rich_text_description": "Ne formatez pas le texte",
"no_blocks": "Aucun bloqués",
"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",
"oauth_tokens": "Jetons OAuth",
"token": "Jeton",
@ -143,7 +268,7 @@
"valid_until": "Valable jusque",
"revoke_token": "Révoquer",
"panelRadius": "Fenêtres",
"pause_on_unfocused": "Suspendre le streaming lorsque l'onglet n'est pas centré",
"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_banner": "Bannière de profil",
@ -152,25 +277,162 @@
"replies_in_timeline": "Réponses au journal",
"reply_link_preview": "Afficher un aperçu lors du survol de liens vers une réponse",
"reply_visibility_all": "Montrer toutes les réponses",
"reply_visibility_following": "Afficher uniquement les réponses adressées à moi ou aux utilisateurs que je suis",
"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",
"autohide_floating_post_button": "Automatiquement cacher le bouton de Nouveau Statut (sur mobile)",
"saving_err": "Erreur lors de l'enregistrement des paramètres",
"saving_ok": "Paramètres enregistrés",
"search_user_to_block": "Rechercher qui vous voulez bloquer",
"search_user_to_mute": "Rechercher qui vous voulez masquer",
"security_tab": "Sécurité",
"scope_copy": "Garder la même visibilité en répondant (les DMs restent toujours des DMs)",
"minimal_scopes_mode": "Rétrécir les options de séléction de la portée",
"set_new_avatar": "Changer d'avatar",
"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_line_behavior": "Copier le sujet en répondant",
"subject_line_email": "Comme les mails: « re: sujet »",
"subject_line_mastodon": "Comme mastodon: copier tel quel",
"subject_line_noop": "Ne pas copier",
"post_status_content_type": "Type de contenu du statuts",
"stop_gifs": "N'animer les GIFS que lors du survol du curseur de la souris",
"streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page",
"text": "Texte",
"theme": "Thème",
"theme_help": "Spécifiez des codes couleur hexadécimaux (#rrvvbb) pour personnaliser les couleurs du thème.",
"theme_help_v2_1": "Vous pouvez aussi surcharger certaines couleurs de composants et transparence via la case à cocher, utilisez le bouton « Vider tout » pour effacer toutes les surcharges.",
"theme_help_v2_2": "Les icônes sous certaines des entrées ont un indicateur de contraste du fond/texte, survolez les pour plus d'informations détailles. Veuillez garder a l'esprit que lors de l'utilisation de transparence l'indicateur de contraste indique le pire des cas.",
"tooltipRadius": "Info-bulles/alertes",
"upload_a_photo": "Envoyer une photo",
"user_settings": "Paramètres utilisateur",
"values": {
"false": "non",
"true": "oui"
},
"notifications": "Notifications",
"notification_setting": "Reçevoir les notifications de:",
"notification_setting_follows": "Utilisateurs que vous suivez",
"notification_setting_non_follows": "Utilisateurs que vous ne suivez pas",
"notification_setting_followers": "Utilisateurs qui vous suivent",
"notification_setting_non_followers": "Utilisateurs qui ne vous suivent pas",
"notification_mutes": "Pour stopper la récéption de notifications d'un utilisateur particulier, utilisez un masquage.",
"notification_blocks": "Bloquer un utilisateur stoppe toute notification et se désabonne de lui.",
"enable_web_push_notifications": "Activer les notifications de push web",
"style": {
"switcher": {
"keep_color": "Garder les couleurs",
"keep_shadows": "Garder les ombres",
"keep_opacity": "Garder la transparence",
"keep_roundness": "Garder la rondeur",
"keep_fonts": "Garder les polices",
"save_load_hint": "L'option « Garder » préserve les options activés en cours lors de la séléction ou chargement des thèmes, il sauve aussi les dites options lors de l'export d'un thème. Quand toutes les cases sont décochés, exporter un thème sauvera tout.",
"reset": "Remise à zéro",
"clear_all": "Tout vider",
"clear_opacity": "Vider la transparence"
},
"common": {
"color": "Couleur",
"opacity": "Transparence",
"contrast": {
"hint": "Le ratio de contraste est {ratio}, il {level} {context}",
"level": {
"aa": "répond aux directives de niveau AA (minimum)",
"aaa": "répond aux directives de niveau AAA (recommandé)",
"bad": "ne réponds à aucune directive d'accessibilité"
},
"context": {
"18pt": "pour texte large (19pt+)",
"text": "pour texte"
}
}
},
"common_colors": {
"_tab_label": "Commun",
"main": "Couleurs communes",
"foreground_hint": "Voir l'onglet « Avancé » pour plus de contrôle détaillé",
"rgbo": "Icônes, accents, badges"
},
"advanced_colors": {
"_tab_label": "Avancé",
"alert": "Fond d'alerte",
"alert_error": "Erreur",
"badge": "Fond de badge",
"badge_notification": "Notification",
"panel_header": "Entête de panneau",
"top_bar": "Barre du haut",
"borders": "Bordures",
"buttons": "Boutons",
"inputs": "Champs de saisie",
"faint_text": "Texte en fondu"
},
"radii": {
"_tab_label": "Rondeur"
},
"shadows": {
"_tab_label": "Ombres et éclairage",
"component": "Composant",
"override": "Surcharger",
"shadow_id": "Ombre #{value}",
"blur": "Flou",
"spread": "Dispersion",
"inset": "Interne",
"hint": "Pour les ombres, vous pouvez aussi utiliser --variable comme valeur de couleur en CSS3. Veuillez noter que spécifier la transparence ne fonctionnera pas dans ce cas.",
"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.",
"spread_zero": "Les ombres avec une dispersion > 0 apparaitrons comme si ils étaient à zéro",
"inset_classic": "L'ombre interne utilisera toujours {0}"
},
"components": {
"panel": "Panneau",
"panelHeader": "En-tête de panneau",
"topBar": "Barre du haut",
"avatar": "Avatar utilisateur⋅ice (dans la vue de profil)",
"avatarStatus": "Avatar utilisateur⋅ice (dans la vue de statuts)",
"popup": "Popups et infobulles",
"button": "Bouton",
"buttonHover": "Bouton (survol)",
"buttonPressed": "Bouton (cliqué)",
"buttonPressedHover": "Bouton (cliqué+survol)",
"input": "Champ de saisie"
}
},
"fonts": {
"_tab_label": "Polices",
"help": "Sélectionnez la police à utiliser pour les éléments de l'UI. Pour « personnalisé » vous avez à entrer le nom exact de la police comme il apparaît dans le système.",
"components": {
"interface": "Interface",
"input": "Champs de saisie",
"post": "Post text",
"postCode": "Texte à taille fixe dans un article (texte enrichi)"
},
"family": "Nom de la police",
"size": "Taille (en px)",
"weight": "Poid (gras)",
"custom": "Personnalisé"
},
"preview": {
"header": "Prévisualisation",
"content": "Contenu",
"error": "Exemple d'erreur",
"button": "Bouton",
"text": "Un certain nombre de {0} et {1}",
"mono": "contenu",
"input": "Je viens juste datterrir à L.A.",
"faint_link": "manuel utile",
"fine_print": "Lisez notre {0} pour n'apprendre rien d'utile !",
"header_faint": "Tout va bien",
"checkbox": "J'ai survolé les conditions d'utilisation",
"link": "un petit lien sympa"
}
},
"version": {
"title": "Version",
"backend_version": "Version du Backend",
"frontend_version": "Version du Frontend"
}
},
"timeline": {
@ -178,32 +440,110 @@
"conversation": "Conversation",
"error_fetching": "Erreur en cherchant les mises à jour",
"load_older": "Afficher plus",
"no_retweet_hint": "Le message est marqué en abonnés-seulement ou direct et ne peut pas être répété",
"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"
"up_to_date": "À jour",
"no_more_statuses": "Pas plus de statuts",
"no_statuses": "Aucun statuts"
},
"status": {
"favorites": "Favoris",
"repeats": "Partages",
"delete": "Supprimer statuts",
"pin": "Agraffer sur le profil",
"unpin": "Dégraffer du profil",
"pinned": "Agraffé",
"delete_confirm": "Voulez-vous vraiment supprimer ce statuts ?",
"reply_to": "Réponse à",
"replies_list": "Réponses:"
},
"user_card": {
"approve": "Accepter",
"block": "Bloquer",
"blocked": "Bloqué !",
"deny": "Rejeter",
"favorites": "Favoris",
"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",
"following": "Suivi !",
"follows_you": "Vous suit !",
"its_you": "C'est vous !",
"media": "Media",
"mute": "Masquer",
"muted": "Masqué",
"per_day": "par jour",
"remote_follow": "Suivre d'une autre instance",
"statuses": "Statuts"
"report": "Signalement",
"statuses": "Statuts",
"unblock": "Débloquer",
"unblock_progress": "Déblocage…",
"block_progress": "Blocage…",
"unmute": "Démasquer",
"unmute_progress": "Démasquage…",
"mute_progress": "Masquage…",
"admin_menu": {
"moderation": "Moderation",
"grant_admin": "Promouvoir Administrateur⋅ice",
"revoke_admin": "Dégrader Administrateur⋅ice",
"grant_moderator": "Promouvoir Modérateur⋅ice",
"revoke_moderator": "Dégrader Modérateur⋅ice",
"activate_account": "Activer le compte",
"deactivate_account": "Désactiver le compte",
"delete_account": "Supprimer le compte",
"force_nsfw": "Marquer tous les statuts comme NSFW",
"strip_media": "Supprimer les medias des statuts",
"force_unlisted": "Forcer les statuts à être délistés",
"sandbox": "Forcer les statuts à être visibles seuleument pour les abonné⋅e⋅s",
"disable_remote_subscription": "Interdir de s'abonner a l'utilisateur depuis l'instance distante",
"disable_any_subscription": "Interdir de s'abonner à l'utilisateur tout court",
"quarantine": "Interdir les statuts de l'utilisateur à fédérer",
"delete_user": "Supprimer l'utilisateur",
"delete_user_confirmation": "Êtes-vous absolument-sûr⋅e ? Cette action ne peut être annulée."
}
},
"user_profile": {
"timeline_title": "Journal de l'utilisateur"
"timeline_title": "Journal de l'utilisateur⋅ice",
"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."
},
"user_reporting": {
"title": "Signaler {0}",
"add_comment_description": "Ce signalement sera envoyé aux modérateur⋅ice⋅s de votre instance. Vous pouvez fournir une explication de pourquoi vous signalez ce compte ci-dessous :",
"additional_comments": "Commentaires additionnels",
"forward_description": "Le compte vient d'un autre serveur. Envoyer une copie du signalement à celui-ci aussi ?",
"forward_to": "Transmettre à {0}",
"submit": "Envoyer",
"generic_error": "Une erreur est survenue lors du traitement de votre requête."
},
"who_to_follow": {
"more": "Plus",
"who_to_follow": "Qui s'abonner"
"who_to_follow": "À qui s'abonner"
},
"tool_tip": {
"media_upload": "Envoyer un media",
"repeat": "Répéter",
"reply": "Répondre",
"favorite": "Favoriser",
"user_settings": "Paramètres utilisateur"
},
"upload": {
"error": {
"base": "L'envoi a échoué.",
"file_too_big": "Fichier trop gros [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Réessayez plus tard"
},
"file_size_units": {
"B": "O",
"KiB": "KiO",
"MiB": "MiO",
"GiB": "GiO",
"TiB": "TiO"
}
}
}

View file

@ -170,6 +170,40 @@
"true": "tá"
}
},
"time": {
"day": "{0} lá",
"days": "{0} lá",
"day_short": "{0}l",
"days_short": "{0}l",
"hour": "{0} uair",
"hours": "{0} uair",
"hour_short": "{0}u",
"hours_short": "{0}u",
"in_future": "in {0}",
"in_past": "{0} ago",
"minute": "{0} nóimeád",
"minutes": "{0} nóimeád",
"minute_short": "{0}n",
"minutes_short": "{0}n",
"month": "{0} mí",
"months": "{0} mí",
"month_short": "{0}m",
"months_short": "{0}m",
"now": "Anois",
"now_short": "Anois",
"second": "{0} s",
"seconds": "{0} s",
"second_short": "{0}s",
"seconds_short": "{0}s",
"week": "{0} seachtain",
"weeks": "{0} seachtaine",
"week_short": "{0}se",
"weeks_short": "{0}se",
"year": "{0} bliainta",
"years": "{0} bliainta",
"year_short": "{0}b",
"years_short": "{0}b"
},
"timeline": {
"collapse": "Folaigh",
"conversation": "Cómhra",

View file

@ -402,6 +402,40 @@
"frontend_version": "フロントエンドのバージョン"
}
},
"time": {
"day": "{0}日",
"days": "{0}日",
"day_short": "{0}日",
"days_short": "{0}日",
"hour": "{0}時間",
"hours": "{0}時間",
"hour_short": "{0}時間",
"hours_short": "{0}時間",
"in_future": "{0}で",
"in_past": "{0}前",
"minute": "{0}分",
"minutes": "{0}分",
"minute_short": "{0}分",
"minutes_short": "{0}分",
"month": "{0}ヶ月前",
"months": "{0}ヶ月前",
"month_short": "{0}ヶ月前",
"months_short": "{0}ヶ月前",
"now": "たった今",
"now_short": "たった今",
"second": "{0}秒",
"seconds": "{0}秒",
"second_short": "{0}秒",
"seconds_short": "{0}秒",
"week": "{0}週間",
"weeks": "{0}週間",
"week_short": "{0}週間",
"weeks_short": "{0}週間",
"year": "{0}年",
"years": "{0}年",
"year_short": "{0}年",
"years_short": "{0}年"
},
"timeline": {
"collapse": "たたむ",
"conversation": "スレッド",

View file

@ -402,6 +402,40 @@
"frontend_version": "フロントエンドのバージョン"
}
},
"time": {
"day": "{0}日",
"days": "{0}日",
"day_short": "{0}日",
"days_short": "{0}日",
"hour": "{0}時間",
"hours": "{0}時間",
"hour_short": "{0}時間",
"hours_short": "{0}時間",
"in_future": "{0}で",
"in_past": "{0}前",
"minute": "{0}分",
"minutes": "{0}分",
"minute_short": "{0}分",
"minutes_short": "{0}分",
"month": "{0}ヶ月前",
"months": "{0}ヶ月前",
"month_short": "{0}ヶ月前",
"months_short": "{0}ヶ月前",
"now": "たった今",
"now_short": "たった今",
"second": "{0}秒",
"seconds": "{0}秒",
"second_short": "{0}秒",
"seconds_short": "{0}秒",
"week": "{0}週間",
"weeks": "{0}週間",
"week_short": "{0}週間",
"weeks_short": "{0}週間",
"year": "{0}年",
"years": "{0}年",
"year_short": "{0}年",
"years_short": "{0}年"
},
"timeline": {
"collapse": "たたむ",
"conversation": "スレッド",

View file

@ -381,6 +381,40 @@
"frontend_version": "Version Frontend"
}
},
"time": {
"day": "{0} jorn",
"days": "{0} jorns",
"day_short": "{0} jorn",
"days_short": "{0} jorns",
"hour": "{0} hour",
"hours": "{0} hours",
"hour_short": "{0}h",
"hours_short": "{0}h",
"in_future": "in {0}",
"in_past": "fa {0}",
"minute": "{0} minute",
"minutes": "{0} minutes",
"minute_short": "{0}min",
"minutes_short": "{0}min",
"month": "{0} mes",
"months": "{0} meses",
"month_short": "{0} mes",
"months_short": "{0} meses",
"now": "ara meteis",
"now_short": "ara meteis",
"second": "{0} second",
"seconds": "{0} seconds",
"second_short": "{0}s",
"seconds_short": "{0}s",
"week": "{0} setm.",
"weeks": "{0} setm.",
"week_short": "{0} setm.",
"weeks_short": "{0} setm.",
"year": "{0} an",
"years": "{0} ans",
"year_short": "{0} an",
"years_short": "{0} ans"
},
"timeline": {
"collapse": "Tampar",
"conversation": "Conversacion",

View file

@ -15,7 +15,6 @@ import mediaViewerModule from './modules/media_viewer.js'
import oauthTokensModule from './modules/oauth_tokens.js'
import reportsModule from './modules/reports.js'
import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n'
import createPersistedState from './lib/persisted_state.js'
@ -33,14 +32,6 @@ const currentLocale = (window.navigator.language || 'en').split('-')[0]
Vue.use(Vuex)
Vue.use(VueRouter)
Vue.use(VueTimeago, {
locale: currentLocale === 'cs' ? 'cs' : currentLocale === 'ja' ? 'ja' : 'en',
locales: {
'cs': require('../static/timeago-cs.json'),
'en': require('../static/timeago-en.json'),
'ja': require('../static/timeago-ja.json')
}
})
Vue.use(VueI18n)
Vue.use(VueChatScroll)
Vue.use(VueClickOutside)

View file

@ -52,7 +52,15 @@ const defaultState = {
// Version Information
backendVersion: '',
frontendVersion: ''
frontendVersion: '',
pollsAvailable: false,
pollLimits: {
max_options: 4,
max_option_chars: 255,
min_expiration: 60,
max_expiration: 60 * 60 * 24
}
}
const instance = {

View file

@ -494,6 +494,10 @@ export const mutations = {
const newStatus = state.allStatusesObject[id]
newStatus.favoritedBy = favoritedByUsers.filter(_ => _)
newStatus.rebloggedBy = rebloggedByUsers.filter(_ => _)
},
updateStatusWithPoll (state, { id, poll }) {
const status = state.allStatusesObject[id]
status.poll = poll
}
}
@ -578,6 +582,18 @@ const statuses = {
]).then(([favoritedByUsers, rebloggedByUsers]) =>
commit('addFavsAndRepeats', { id, favoritedByUsers, rebloggedByUsers })
)
},
votePoll ({ rootState, commit }, { id, pollId, choices }) {
return rootState.api.backendInteractor.vote(pollId, choices).then(poll => {
commit('updateStatusWithPoll', { id, poll })
return poll
})
},
refreshPoll ({ rootState, commit }, { id, pollId }) {
return rootState.api.backendInteractor.fetchPoll(pollId).then(poll => {
commit('updateStatusWithPoll', { id, poll })
return poll
})
}
},
mutations

View file

@ -1,3 +1,8 @@
import { each, map, concat, last } from 'lodash'
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch'
import { StatusCodeError } from '../errors/errors'
/* eslint-env browser */
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
@ -52,6 +57,8 @@ const MASTODON_MUTE_USER_URL = id => `/api/v1/accounts/${id}/mute`
const MASTODON_UNMUTE_USER_URL = id => `/api/v1/accounts/${id}/unmute`
const MASTODON_POST_STATUS_URL = '/api/v1/statuses'
const MASTODON_MEDIA_UPLOAD_URL = '/api/v1/media'
const MASTODON_VOTE_URL = id => `/api/v1/polls/${id}/votes`
const MASTODON_POLL_URL = id => `/api/v1/polls/${id}`
const MASTODON_STATUS_FAVORITEDBY_URL = id => `/api/v1/statuses/${id}/favourited_by`
const MASTODON_STATUS_REBLOGGEDBY_URL = id => `/api/v1/statuses/${id}/reblogged_by`
const MASTODON_PROFILE_UPDATE_URL = '/api/v1/accounts/update_credentials'
@ -59,11 +66,6 @@ const MASTODON_REPORT_USER_URL = '/api/v1/reports'
const MASTODON_PIN_OWN_STATUS = id => `/api/v1/statuses/${id}/pin`
const MASTODON_UNPIN_OWN_STATUS = id => `/api/v1/statuses/${id}/unpin`
import { each, map, concat, last } from 'lodash'
import { parseStatus, parseUser, parseNotification, parseAttachment } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch'
import { StatusCodeError } from '../errors/errors'
const oldfetch = window.fetch
let fetch = (url, options) => {
@ -115,8 +117,7 @@ const updateNotificationSettings = ({credentials, settings}) => {
headers: authHeaders(credentials),
method: 'PUT',
body: form
})
.then((data) => data.json())
}).then((data) => data.json())
}
const updateAvatar = ({ credentials, avatar }) => {
@ -126,8 +127,7 @@ const updateAvatar = ({credentials, avatar}) => {
headers: authHeaders(credentials),
method: 'PATCH',
body: form
})
.then((data) => data.json())
}).then((data) => data.json())
.then((data) => parseUser(data))
}
@ -150,8 +150,7 @@ const updateBanner = ({credentials, banner}) => {
headers: authHeaders(credentials),
method: 'PATCH',
body: form
})
.then((data) => data.json())
}).then((data) => data.json())
.then((data) => parseUser(data))
}
@ -161,8 +160,7 @@ const updateProfile = ({credentials, params}) => {
method: 'PATCH',
payload: params,
credentials
})
.then((data) => parseUser(data))
}).then((data) => parseUser(data))
}
// Params needed:
@ -459,7 +457,15 @@ const deleteUser = ({credentials, ...user}) => {
})
}
const fetchTimeline = ({timeline, credentials, since = false, until = false, userId = false, tag = false, withMuted = false}) => {
const fetchTimeline = ({
timeline,
credentials,
since = false,
until = false,
userId = false,
tag = false,
withMuted = false
}) => {
const timelineUrls = {
public: MASTODON_PUBLIC_TIMELINE,
friends: MASTODON_USER_HOME_TIMELINE_URL,
@ -558,8 +564,19 @@ const unretweet = ({ id, credentials }) => {
.then((data) => parseStatus(data))
}
const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds = [], inReplyToStatusId, contentType}) => {
const postStatus = ({
credentials,
status,
spoilerText,
visibility,
sensitive,
poll,
mediaIds = [],
inReplyToStatusId,
contentType
}) => {
const form = new FormData()
const pollOptions = poll.options || []
form.append('status', status)
form.append('source', 'Pleroma FE')
@ -570,6 +587,19 @@ const postStatus = ({credentials, status, spoilerText, visibility, sensitive, me
mediaIds.forEach(val => {
form.append('media_ids[]', val)
})
if (pollOptions.some(option => option !== '')) {
const normalizedPoll = {
expires_in: poll.expiresIn,
multiple: poll.multiple
}
Object.keys(normalizedPoll).forEach(key => {
form.append(`poll[${key}]`, normalizedPoll[key])
})
pollOptions.forEach(option => {
form.append('poll[options][]', option)
})
}
if (inReplyToStatusId) {
form.append('in_reply_to_id', inReplyToStatusId)
}
@ -761,6 +791,30 @@ const markNotificationsAsSeen = ({id, credentials}) => {
}).then((data) => data.json())
}
const vote = ({ pollId, choices, credentials }) => {
const form = new FormData()
form.append('choices', choices)
return promisedRequest({
url: MASTODON_VOTE_URL(encodeURIComponent(pollId)),
method: 'POST',
credentials,
payload: {
choices: choices
}
})
}
const fetchPoll = ({ pollId, credentials }) => {
return promisedRequest(
{
url: MASTODON_POLL_URL(encodeURIComponent(pollId)),
method: 'GET',
credentials
}
)
}
const fetchFavoritedByUsers = ({ id }) => {
return promisedRequest({ url: MASTODON_STATUS_FAVORITEDBY_URL(id) }).then((users) => users.map(parseUser))
}
@ -840,6 +894,8 @@ const apiService = {
denyUser,
suggestions,
markNotificationsAsSeen,
vote,
fetchPoll,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser,

View file

@ -2,7 +2,7 @@ import apiService from '../api/api.service.js'
import timelineFetcherService from '../timeline_fetcher/timeline_fetcher.service.js'
import notificationsFetcher from '../notifications_fetcher/notifications_fetcher.service.js'
const backendInteractorService = (credentials) => {
const backendInteractorService = credentials => {
const fetchStatus = ({ id }) => {
return apiService.fetchStatus({ id, credentials })
}
@ -87,6 +87,14 @@ const backendInteractorService = (credentials) => {
return apiService.deleteUser({ screen_name, credentials })
}
const vote = (pollId, choices) => {
return apiService.vote({ credentials, pollId, choices })
}
const fetchPoll = (pollId) => {
return apiService.fetchPoll({ credentials, pollId })
}
const updateNotificationSettings = ({ settings }) => {
return apiService.updateNotificationSettings({ credentials, settings })
}
@ -110,11 +118,13 @@ const backendInteractorService = (credentials) => {
const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params })
const externalProfile = (profileUrl) => apiService.externalProfile({ profileUrl, credentials })
const importBlocks = (file) => apiService.importBlocks({ file, credentials })
const importFollows = (file) => apiService.importFollows({ file, credentials })
const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password })
const changePassword = ({password, newPassword, newPasswordConfirmation}) => apiService.changePassword({credentials, password, newPassword, newPasswordConfirmation})
const changePassword = ({ password, newPassword, newPasswordConfirmation }) =>
apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation })
const fetchSettingsMFA = () => apiService.settingsMFA({ credentials })
const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials })
@ -180,6 +190,8 @@ const backendInteractorService = (credentials) => {
fetchFollowRequests,
approveUser,
denyUser,
vote,
fetchPoll,
fetchFavoritedByUsers,
fetchRebloggedByUsers,
reportUser,

View file

@ -0,0 +1,45 @@
export const SECOND = 1000
export const MINUTE = 60 * SECOND
export const HOUR = 60 * MINUTE
export const DAY = 24 * HOUR
export const WEEK = 7 * DAY
export const MONTH = 30 * DAY
export const YEAR = 365.25 * DAY
export const relativeTime = (date, nowThreshold = 1) => {
if (typeof date === 'string') date = Date.parse(date)
const round = Date.now() > date ? Math.floor : Math.ceil
const d = Math.abs(Date.now() - date)
let r = { num: round(d / YEAR), key: 'time.years' }
if (d < nowThreshold * SECOND) {
r.num = 0
r.key = 'time.now'
} else if (d < MINUTE) {
r.num = round(d / SECOND)
r.key = 'time.seconds'
} else if (d < HOUR) {
r.num = round(d / MINUTE)
r.key = 'time.minutes'
} else if (d < DAY) {
r.num = round(d / HOUR)
r.key = 'time.hours'
} else if (d < WEEK) {
r.num = round(d / DAY)
r.key = 'time.days'
} else if (d < MONTH) {
r.num = round(d / WEEK)
r.key = 'time.weeks'
} else if (d < YEAR) {
r.num = round(d / MONTH)
r.key = 'time.months'
}
// Remove plural form when singular
if (r.num === 1) r.key = r.key.slice(0, -1)
return r
}
export const relativeTimeShort = (date, nowThreshold = 1) => {
const r = relativeTime(date, nowThreshold)
r.key += '_short'
return r
}

View file

@ -234,6 +234,7 @@ export const parseStatus = (data) => {
output.summary_html = addEmojis(data.spoiler_text, data.emojis)
output.external_url = data.url
output.poll = data.poll
output.pinned = data.pinned
} else {
output.favorited = data.favorited

View file

@ -1,10 +1,19 @@
import { map } from 'lodash'
import apiService from '../api/api.service.js'
const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
const postStatus = ({ store, status, spoilerText, visibility, sensitive, poll, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
const mediaIds = map(media, 'id')
return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType})
return apiService.postStatus({
credentials: store.state.users.currentUser.credentials,
status,
spoilerText,
visibility,
sensitive,
mediaIds,
inReplyToStatusId,
contentType,
poll})
.then((data) => {
if (!data.error) {
store.dispatch('addNewStatuses', {

View file

@ -202,6 +202,7 @@ const generateColors = (input) => {
colors.topBarLink = col.topBarLink || getTextColor(colors.topBar, colors.fgLink)
colors.faintLink = col.faintLink || Object.assign({}, col.link)
colors.linkBg = alphaBlend(colors.link, 0.4, colors.bg)
colors.icon = mixrgb(colors.bg, colors.text)

0
static/font/LICENSE.txt Executable file → Normal file
View file

0
static/font/README.txt Executable file → Normal file
View file

6
static/font/config.json Executable file → Normal file
View file

@ -240,6 +240,12 @@
"code": 59416,
"src": "fontawesome"
},
{
"uid": "266d5d9adf15a61800477a5acf9a4462",
"css": "chart-bar",
"code": 59419,
"src": "fontawesome"
},
{
"uid": "671f29fa10dda08074a4c6a341bb4f39",
"css": "bell-alt",

View file

@ -26,6 +26,7 @@
.icon-pencil:before { content: '\e818'; } /* '' */
.icon-pin:before { content: '\e819'; } /* '' */
.icon-wrench:before { content: '\e81a'; } /* '' */
.icon-chart-bar:before { content: '\e81b'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */

File diff suppressed because one or more lines are too long

View file

@ -26,6 +26,7 @@
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }

View file

@ -37,6 +37,7 @@
.icon-pencil { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe818;&nbsp;'); }
.icon-pin { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe819;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81a;&nbsp;'); }
.icon-chart-bar { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe81b;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&nbsp;'); }

View file

@ -1,11 +1,11 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?16609299');
src: url('../font/fontello.eot?16609299#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?16609299') format('woff2'),
url('../font/fontello.woff?16609299') format('woff'),
url('../font/fontello.ttf?16609299') format('truetype'),
url('../font/fontello.svg?16609299#fontello') format('svg');
src: url('../font/fontello.eot?3304725');
src: url('../font/fontello.eot?3304725#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?3304725') format('woff2'),
url('../font/fontello.woff?3304725') format('woff'),
url('../font/fontello.ttf?3304725') format('truetype'),
url('../font/fontello.svg?3304725#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?16609299#fontello') format('svg');
src: url('../font/fontello.svg?3304725#fontello') format('svg');
}
}
*/
@ -82,6 +82,7 @@
.icon-pencil:before { content: '\e818'; } /* '' */
.icon-pin:before { content: '\e819'; } /* '' */
.icon-wrench:before { content: '\e81a'; } /* '' */
.icon-chart-bar:before { content: '\e81b'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */

19
static/font/demo.html Executable file → Normal file
View file

@ -229,11 +229,11 @@ body {
}
@font-face {
font-family: 'fontello';
src: url('./font/fontello.eot?79958594');
src: url('./font/fontello.eot?79958594#iefix') format('embedded-opentype'),
url('./font/fontello.woff?79958594') format('woff'),
url('./font/fontello.ttf?79958594') format('truetype'),
url('./font/fontello.svg?79958594#fontello') format('svg');
src: url('./font/fontello.eot?14310629');
src: url('./font/fontello.eot?14310629#iefix') format('embedded-opentype'),
url('./font/fontello.woff?14310629') format('woff'),
url('./font/fontello.ttf?14310629') format('truetype'),
url('./font/fontello.svg?14310629#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@ -337,27 +337,28 @@ body {
<div class="the-icons span3" title="Code: 0xe818"><i class="demo-icon icon-pencil">&#xe818;</i> <span class="i-name">icon-pencil</span><span class="i-code">0xe818</span></div>
<div class="the-icons span3" title="Code: 0xe819"><i class="demo-icon icon-pin">&#xe819;</i> <span class="i-name">icon-pin</span><span class="i-code">0xe819</span></div>
<div class="the-icons span3" title="Code: 0xe81a"><i class="demo-icon icon-wrench">&#xe81a;</i> <span class="i-name">icon-wrench</span><span class="i-code">0xe81a</span></div>
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
<div class="the-icons span3" title="Code: 0xe81b"><i class="demo-icon icon-chart-bar">&#xe81b;</i> <span class="i-name">icon-chart-bar</span><span class="i-code">0xe81b</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt">&#xf08f;</i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
<div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty">&#xf0e5;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
<div class="the-icons span3" title="Code: 0xf0f3"><i class="demo-icon icon-bell-alt">&#xf0f3;</i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</span></div>
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
<div class="the-icons span3" title="Code: 0xf141"><i class="demo-icon icon-ellipsis">&#xf141;</i> <span class="i-name">icon-ellipsis</span><span class="i-code">0xf141</span></div>
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled">&#xf144;</i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled">&#xf144;</i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt">&#xf164;</i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
<div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>

Binary file not shown.

View file

@ -60,6 +60,8 @@
<glyph glyph-name="wrench" unicode="&#xe81a;" d="M214 36q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
<glyph glyph-name="chart-bar" unicode="&#xe81b;" d="M357 357v-286h-143v286h143z m214 286v-572h-142v572h142z m572-643v-72h-1143v858h71v-786h1072z m-357 500v-429h-143v429h143z m214 214v-643h-143v643h143z" horiz-adv-x="1142.9" />
<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,10 +0,0 @@
[
"ara mateix",
["fa %s s", "fa %s s"],
["fa %s min", "fa %s min"],
["fa %s h", "fa %s h"],
["fa %s dia", "fa %s dies"],
["fa %s setm.", "fa %s setm."],
["fa %s mes", "fa %s mesos"],
["fa %s any", "fa %s anys"]
]

View file

@ -1,10 +0,0 @@
[
"teď",
["%s s", "%s s"],
["%s min", "%s min"],
["%s h", "%s h"],
["%s d", "%s d"],
["%s týd", "%s týd"],
["%s měs", "%s měs"],
["%s r", "%s l"]
]

View file

@ -1,10 +0,0 @@
[
"now",
["%ss", "%ss"],
["%smin", "%smin"],
["%sh", "%sh"],
["%sd", "%sd"],
["%sw", "%sw"],
["%smo", "%smo"],
["%sy", "%sy"]
]

View file

@ -1,10 +0,0 @@
[
"Anois",
["%s s", "%s s"],
["%s n", "%s nóimeád"],
["%s u", "%s uair"],
["%s l", "%s lá"],
["%s se", "%s seachtaine"],
["%s m", "%s mí"],
["%s b", "%s bliainta"]
]

View file

@ -1,10 +0,0 @@
[
"たった今",
"%s 秒前",
"%s 分前",
"%s 時間前",
"%s 日前",
"%s 週間前",
"%s ヶ月前",
"%s 年前"
]

View file

@ -1,10 +0,0 @@
[
"ara meteis",
["fa %s s", "fa %s s"],
["fa %s min", "fa %s min"],
["fa %s h", "fa %s h"],
["fa %s jorn", "fa %s jorns"],
["fa %s setm.", "fa %s setm."],
["fa %s mes", "fa %s meses"],
["fa %s an", "fa %s ans"]
]

View file

@ -0,0 +1,40 @@
import * as DateUtils from 'src/services/date_utils/date_utils.js'
describe('DateUtils', () => {
describe('relativeTime', () => {
it('returns now with low enough amount of seconds', () => {
const futureTime = Date.now() + 20 * DateUtils.SECOND
const pastTime = Date.now() - 20 * DateUtils.SECOND
expect(DateUtils.relativeTime(futureTime, 30)).to.eql({ num: 0, key: 'time.now' })
expect(DateUtils.relativeTime(pastTime, 30)).to.eql({ num: 0, key: 'time.now' })
})
it('rounds down for past', () => {
const time = Date.now() - 1.8 * DateUtils.HOUR
expect(DateUtils.relativeTime(time)).to.eql({ num: 1, key: 'time.hour' })
})
it('rounds up for future', () => {
const time = Date.now() + 1.8 * DateUtils.HOUR
expect(DateUtils.relativeTime(time)).to.eql({ num: 2, key: 'time.hours' })
})
it('uses plural when necessary', () => {
const time = Date.now() - 3.8 * DateUtils.WEEK
expect(DateUtils.relativeTime(time)).to.eql({ num: 3, key: 'time.weeks' })
})
it('works with date string', () => {
const time = Date.now() - 4 * DateUtils.MONTH
const dateString = new Date(time).toISOString()
expect(DateUtils.relativeTime(dateString)).to.eql({ num: 4, key: 'time.months' })
})
})
describe('relativeTimeShort', () => {
it('returns the short version of the same relative time', () => {
const time = Date.now() + 2 * DateUtils.YEAR
expect(DateUtils.relativeTimeShort(time)).to.eql({ num: 2, key: 'time.years_short' })
})
})
})

1183
yarn.lock

File diff suppressed because it is too large Load diff