Merge branch 'master' into glitch-soc/merge-upstream

staging
Thibaut Girka 2018-10-28 08:37:49 +01:00
commit b00f60f1d3
44 changed files with 267 additions and 256 deletions

View File

@ -178,7 +178,7 @@ jobs:
- *attach_workspace - *attach_workspace
- run: bundle exec i18n-tasks check-normalized - run: bundle exec i18n-tasks check-normalized
- run: bundle exec i18n-tasks unused - run: bundle exec i18n-tasks unused
- run: bundle exec i18n-tasks missing-plural-keys - run: bundle exec i18n-tasks missing -t plural
- run: bundle exec i18n-tasks check-consistent-interpolations - run: bundle exec i18n-tasks check-consistent-interpolations
workflows: workflows:

View File

@ -96,7 +96,7 @@ gem 'rdf-normalize', '~> 0.3'
group :development, :test do group :development, :test do
gem 'fabrication', '~> 2.20' gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.3' gem 'fuubar', '~> 2.3'
gem 'i18n-tasks', '~> 0.9', require: false, git: 'https://github.com/Gargron/i18n-tasks.git', ref: '7a57fbe7000f4f8120e250a757ab345c28c6885c' gem 'i18n-tasks', '~> 0.9', require: false, git: 'https://github.com/Gargron/i18n-tasks.git', ref: 'ab6e10878ccdb6243f934f30372276d260c14251'
gem 'pry-byebug', '~> 3.6' gem 'pry-byebug', '~> 3.6'
gem 'pry-rails', '~> 0.3' gem 'pry-rails', '~> 0.3'
gem 'rspec-rails', '~> 3.8' gem 'rspec-rails', '~> 3.8'

View File

@ -1,7 +1,7 @@
GIT GIT
remote: https://github.com/Gargron/i18n-tasks.git remote: https://github.com/Gargron/i18n-tasks.git
revision: 7a57fbe7000f4f8120e250a757ab345c28c6885c revision: ab6e10878ccdb6243f934f30372276d260c14251
ref: 7a57fbe7000f4f8120e250a757ab345c28c6885c ref: ab6e10878ccdb6243f934f30372276d260c14251
specs: specs:
i18n-tasks (0.9.27) i18n-tasks (0.9.27)
activesupport (>= 4.0.2) activesupport (>= 4.0.2)

View File

@ -201,6 +201,7 @@ class ApplicationController < ActionController::Base
def respond_with_error(code) def respond_with_error(code)
respond_to do |format| respond_to do |format|
format.any { head code } format.any { head code }
format.html do format.html do
set_locale set_locale
use_pack 'error' use_pack 'error'

View File

@ -9,6 +9,7 @@ import DisplayName from './display_name';
import StatusContent from './status_content'; import StatusContent from './status_content';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list'; import AttachmentList from './attachment_list';
import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl'; import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video } from '../features/ui/util/async-components'; import { MediaGallery, Video } from '../features/ui/util/async-components';
@ -256,6 +257,14 @@ class Status extends ImmutablePureComponent {
</Bundle> </Bundle>
); );
} }
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
media = (
<Card
onOpenMedia={this.props.onOpenMedia}
card={status.get('card')}
compact
/>
);
} }
if (otherAccounts) { if (otherAccounts) {

View File

@ -2,7 +2,7 @@
// https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js // https://github.com/missive/emoji-mart/blob/5f2ffcc/src/utils/emoji-index.js
import data from './emoji_mart_data_light'; import data from './emoji_mart_data_light';
import { getData, getSanitizedData, intersect } from './emoji_utils'; import { getData, getSanitizedData, uniq, intersect } from './emoji_utils';
let originalPool = {}; let originalPool = {};
let index = {}; let index = {};
@ -103,7 +103,7 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
} }
} }
allResults = values.map((value) => { const searchValue = (value) => {
let aPool = pool, let aPool = pool,
aIndex = index, aIndex = index,
length = 0; length = 0;
@ -150,15 +150,23 @@ function search(value, { emojisToShowFilter, maxResults, include, exclude, custo
} }
return aIndex.results; return aIndex.results;
}).filter(a => a); };
if (allResults.length > 1) { if (values.length > 1) {
results = intersect.apply(null, allResults); results = searchValue(value);
} else if (allResults.length) {
results = allResults[0];
} else { } else {
results = []; results = [];
} }
allResults = values.map(searchValue).filter(a => a);
if (allResults.length > 1) {
allResults = intersect.apply(null, allResults);
} else if (allResults.length) {
allResults = allResults[0];
}
results = uniq(results.concat(allResults));
} }
if (results) { if (results) {

View File

@ -59,10 +59,12 @@ export default class Card extends React.PureComponent {
card: ImmutablePropTypes.map, card: ImmutablePropTypes.map,
maxDescription: PropTypes.number, maxDescription: PropTypes.number,
onOpenMedia: PropTypes.func.isRequired, onOpenMedia: PropTypes.func.isRequired,
compact: PropTypes.boolean,
}; };
static defaultProps = { static defaultProps = {
maxDescription: 50, maxDescription: 50,
compact: false,
}; };
state = { state = {
@ -131,25 +133,25 @@ export default class Card extends React.PureComponent {
} }
render () { render () {
const { card, maxDescription } = this.props; const { card, maxDescription, compact } = this.props;
const { width, embedded } = this.state; const { width, embedded } = this.state;
if (card === null) { if (card === null) {
return null; return null;
} }
const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width) || card.get('type') !== 'link'; const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded;
const className = classnames('status-card', { horizontal });
const interactive = card.get('type') !== 'link'; const interactive = card.get('type') !== 'link';
const className = classnames('status-card', { horizontal, compact, interactive });
const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>;
const ratio = card.get('width') / card.get('height'); const ratio = compact ? 16 / 9 : card.get('width') / card.get('height');
const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio); const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio);
const description = ( const description = (
<div className='status-card__content'> <div className='status-card__content'>
{title} {title}
{!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>} {!(horizontal || compact) && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>}
<span className='status-card__host'>{provider}</span> <span className='status-card__host'>{provider}</span>
</div> </div>
); );
@ -174,7 +176,7 @@ export default class Card extends React.PureComponent {
<div className='status-card__actions'> <div className='status-card__actions'>
<div> <div>
<button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button> <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button>
<a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a> {horizontal && <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a>}
</div> </div>
</div> </div>
</div> </div>
@ -184,7 +186,7 @@ export default class Card extends React.PureComponent {
return ( return (
<div className={className} ref={this.setRef}> <div className={className} ref={this.setRef}>
{embed} {embed}
{description} {!compact && description}
</div> </div>
); );
} else if (card.get('image')) { } else if (card.get('image')) {

View File

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import Card from '../components/card'; import Card from '../components/card';
const mapStateToProps = (state, { statusId }) => ({ const mapStateToProps = (state, { statusId }) => ({
card: state.getIn(['cards', statusId], null), card: state.getIn(['statuses', statusId, 'card'], null),
}); });
export default connect(mapStateToProps)(Card); export default connect(mapStateToProps)(Card);

View File

@ -14,7 +14,6 @@ import relationships from './relationships';
import settings from './settings'; import settings from './settings';
import push_notifications from './push_notifications'; import push_notifications from './push_notifications';
import status_lists from './status_lists'; import status_lists from './status_lists';
import cards from './cards';
import mutes from './mutes'; import mutes from './mutes';
import reports from './reports'; import reports from './reports';
import contexts from './contexts'; import contexts from './contexts';
@ -46,7 +45,6 @@ const reducers = {
relationships, relationships,
settings, settings,
push_notifications, push_notifications,
cards,
mutes, mutes,
reports, reports,
contexts, contexts,

View File

@ -10,6 +10,7 @@ import {
STATUS_REVEAL, STATUS_REVEAL,
STATUS_HIDE, STATUS_HIDE,
} from '../actions/statuses'; } from '../actions/statuses';
import { STATUS_CARD_FETCH_SUCCESS } from '../actions/cards';
import { TIMELINE_DELETE } from '../actions/timelines'; import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
import { Map as ImmutableMap, fromJS } from 'immutable'; import { Map as ImmutableMap, fromJS } from 'immutable';
@ -65,6 +66,8 @@ export default function statuses(state = initialState, action) {
}); });
case TIMELINE_DELETE: case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references); return deleteStatus(state, action.id, action.references);
case STATUS_CARD_FETCH_SUCCESS:
return state.setIn([action.id, 'card'], fromJS(action.card));
default: default:
return state; return state;
} }

View File

@ -1669,6 +1669,7 @@ a.account__display-name {
padding: 4px 0; padding: 4px 0;
border-radius: 4px; border-radius: 4px;
box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4);
z-index: 9999;
ul { ul {
list-style: none; list-style: none;
@ -2560,6 +2561,9 @@ a.status-card {
display: block; display: block;
margin-top: 5px; margin-top: 5px;
font-size: 13px; font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
.status-card__image { .status-card__image {
@ -2584,6 +2588,31 @@ a.status-card {
} }
} }
.status-card.compact {
border-color: lighten($ui-base-color, 4%);
&.interactive {
border: 0;
}
.status-card__content {
padding: 8px;
padding-top: 10px;
}
.status-card__title {
white-space: nowrap;
}
.status-card__image {
flex: 0 0 60px;
}
}
a.status-card.compact:hover {
background-color: lighten($ui-base-color, 4%);
}
.status-card__image-image { .status-card__image-image {
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
display: block; display: block;

View File

@ -148,6 +148,7 @@ class MediaAttachment < ApplicationRecord
"#{x},#{y}" "#{x},#{y}"
end end
after_commit :reset_parent_cache, on: :update
before_create :prepare_description, unless: :local? before_create :prepare_description, unless: :local?
before_create :set_shortcode before_create :set_shortcode
before_post_process :set_type_and_extension before_post_process :set_type_and_extension
@ -252,4 +253,9 @@ class MediaAttachment < ApplicationRecord
bitrate: movie.bitrate, bitrate: movie.bitrate,
} }
end end
def reset_parent_cache
return if status_id.nil?
Rails.cache.delete("statuses/#{status_id}")
end
end end

View File

@ -94,6 +94,7 @@ class Status < ApplicationRecord
:conversation, :conversation,
:status_stat, :status_stat,
:tags, :tags,
:preview_cards,
:stream_entry, :stream_entry,
active_mentions: :account, active_mentions: :account,
reblog: [ reblog: [
@ -101,6 +102,7 @@ class Status < ApplicationRecord
:application, :application,
:stream_entry, :stream_entry,
:tags, :tags,
:preview_cards,
:media_attachments, :media_attachments,
:conversation, :conversation,
:status_stat, :status_stat,
@ -168,6 +170,10 @@ class Status < ApplicationRecord
reblog reblog
end end
def preview_card
preview_cards.first
end
def title def title
if destroyed? if destroyed?
"#{account.acct} deleted status" "#{account.acct} deleted status"
@ -241,10 +247,6 @@ class Status < ApplicationRecord
before_validation :set_local before_validation :set_local
class << self class << self
def cache_ids
left_outer_joins(:status_stat).select('statuses.id, greatest(statuses.updated_at, status_stats.updated_at) AS updated_at')
end
def selectable_visibilities def selectable_visibilities
visibilities.keys - %w(direct limited) visibilities.keys - %w(direct limited)
end end

View File

@ -14,4 +14,12 @@
class StatusStat < ApplicationRecord class StatusStat < ApplicationRecord
belongs_to :status, inverse_of: :status_stat belongs_to :status, inverse_of: :status_stat
after_commit :reset_parent_cache
private
def reset_parent_cache
Rails.cache.delete("statuses/#{status_id}")
end
end end

View File

@ -21,6 +21,8 @@ class REST::StatusSerializer < ActiveModel::Serializer
has_many :tags has_many :tags
has_many :emojis, serializer: REST::CustomEmojiSerializer has_many :emojis, serializer: REST::CustomEmojiSerializer
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
def id def id
object.id.to_s object.id.to_s
end end

View File

@ -63,6 +63,7 @@ class FetchLinkCardService < BaseService
def attach_card def attach_card
@status.preview_cards << @card @status.preview_cards << @card
Rails.cache.delete(@status)
end end
def parse_urls def parse_urls

View File

@ -1 +1,2 @@
---
ast: {} ast: {}

View File

@ -21,8 +21,7 @@ ast:
hosted_on: Mastodon ta agospiáu en %{domain} hosted_on: Mastodon ta agospiáu en %{domain}
learn_more: Deprendi más learn_more: Deprendi más
source_code: Códigu fonte source_code: Códigu fonte
status_count_after: status_count_after: estaos
other: estaos
terms: Términos del serviciu terms: Términos del serviciu
user_count_after: usuarios user_count_after: usuarios
what_is_mastodon: "¿Qué ye Mastodon?" what_is_mastodon: "¿Qué ye Mastodon?"

View File

@ -30,22 +30,16 @@ cs:
other_instances: Seznam instancí other_instances: Seznam instancí
privacy_policy: Zásady soukromí privacy_policy: Zásady soukromí
source_code: Zdrojový kód source_code: Zdrojový kód
status_count_after: status_count_after: příspěvků
one: příspěvek
other: příspěvků
status_count_before: Kteří napsali status_count_before: Kteří napsali
terms: Podmínky používání terms: Podmínky používání
user_count_after: user_count_after: uživatelů
one: uživatele
other: uživatelů
user_count_before: Domov user_count_before: Domov
what_is_mastodon: Co je Mastodon? what_is_mastodon: Co je Mastodon?
accounts: accounts:
choices_html: 'Volby uživatele %{name}:' choices_html: 'Volby uživatele %{name}:'
follow: Sledovat follow: Sledovat
followers: followers: Sledovatelé
one: Sledovatel
other: Sledovatelé
following: Sledovaní following: Sledovaní
joined: Připojil/a se v %{date} joined: Připojil/a se v %{date}
link_verified_on: Vlastnictví tohoto odkazu bylo zkontrolováno %{date} link_verified_on: Vlastnictví tohoto odkazu bylo zkontrolováno %{date}
@ -57,9 +51,7 @@ cs:
people_who_follow: Lidé, kteří sledují uživatele %{name} people_who_follow: Lidé, kteří sledují uživatele %{name}
pin_errors: pin_errors:
following: Musíte již sledovat osobu, kterou chcete podpořit following: Musíte již sledovat osobu, kterou chcete podpořit
posts: posts: Tooty
one: Toot
other: Tooty
posts_tab_heading: Tooty posts_tab_heading: Tooty
posts_with_replies: Tooty a odpovědi posts_with_replies: Tooty a odpovědi
reserved_username: Toto uživatelské jméno je rezervováno reserved_username: Toto uživatelské jméno je rezervováno
@ -268,9 +260,7 @@ cs:
suspend: Suspendovat suspend: Suspendovat
severity: Přísnost severity: Přísnost
show: show:
affected_accounts: affected_accounts: "%{count} účtů v databázi byl ovlivněn"
one: Jeden účet v databázi byl ovlivněn
other: "%{count} účtů v databázi byl ovlivněn"
retroactive: retroactive:
silence: Odtišit všechny existující účty z této domény silence: Odtišit všechny existující účty z této domény
suspend: Zrušit suspenzaci všech existujících účtů z této domény suspend: Zrušit suspenzaci všech existujících účtů z této domény
@ -562,9 +552,7 @@ cs:
followers_count: Počet sledovatelů followers_count: Počet sledovatelů
lock_link: Zamkněte svůj účet lock_link: Zamkněte svůj účet
purge: Odstranit ze sledovatelů purge: Odstranit ze sledovatelů
success: success: V průběhu utišování sledovatelů z %{count} domén...
one: V průběhu utišování sledovatelů z jedné domény...
other: V průběhu utišování sledovatelů z %{count} domén...
true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>. true_privacy_html: Berte prosím na vědomí, že <strong>skutečného soukromí se dá dosáhnout pouze za pomoci end-to-end šifrování</strong>.
unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé příspěvky. %{lock_link}, abyste mohl/a zkontrolovat a odmítnout sledovatele. unlocked_warning_html: Kdokoliv vás může sledovat a okamžitě vidět vaše soukromé příspěvky. %{lock_link}, abyste mohl/a zkontrolovat a odmítnout sledovatele.
unlocked_warning_title: Váš účet není zamknutý unlocked_warning_title: Váš účet není zamknutý
@ -575,9 +563,7 @@ cs:
generic: generic:
changes_saved_msg: Změny byly úspěšně uloženy! changes_saved_msg: Změny byly úspěšně uloženy!
save_changes: Uložit změny save_changes: Uložit změny
validation_errors: validation_errors: Něco ještě není úplně v pořádku! Prosím zkontrolujte %{count} chyb níže
one: Něco ještě není úplně v pořádku! Prosím zkontrolujte chybu níže
other: Něco ještě není úplně v pořádku! Prosím zkontrolujte %{count} chyb níže
imports: imports:
preface: Můžete importovat data, která jste exportoval/a z jiné instance, jako například seznam lidí, které sledujete či blokujete. preface: Můžete importovat data, která jste exportoval/a z jiné instance, jako například seznam lidí, které sledujete či blokujete.
success: Vaše data byla úspěšně nahrána a nyní budou zpracována v daný čas success: Vaše data byla úspěšně nahrána a nyní budou zpracována v daný čas
@ -600,9 +586,7 @@ cs:
expires_in_prompt: Nikdy expires_in_prompt: Nikdy
generate: Vygenerovat generate: Vygenerovat
invited_by: 'Byl/a jste pozván/a uživatelem:' invited_by: 'Byl/a jste pozván/a uživatelem:'
max_uses: max_uses: "%{count} použití"
one: 1 použití
other: "%{count} použití"
max_uses_prompt: Bez limitu max_uses_prompt: Bez limitu
prompt: Vygenerujte a sdílejte s ostatními odkazy a umožněte jim přístup na tuto instanci prompt: Vygenerujte a sdílejte s ostatními odkazy a umožněte jim přístup na tuto instanci
table: table:
@ -628,12 +612,8 @@ cs:
action: Zobrazit všechna oznámení action: Zobrazit všechna oznámení
body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since} body: Zde najdete stručný souhrn zpráv, které jste zmeškal/a od vaší poslední návštěvy %{since}
mention: "%{name} vás zmínil/a v:" mention: "%{name} vás zmínil/a v:"
new_followers_summary: new_followers_summary: Navíc jste získal/a %{count} nových sledovatelů, zatímco jste byl/a pryč! Hurá!
one: Navíc jste získal/a jednoho nového sledovatele, zatímco jste byl/a pryč! Hurá! subject: "%{count} nových oznámení od vaší poslední návštěvy \U0001F418"
other: Navíc jste získal/a %{count} nových sledovatelů, zatímco jste byl/a pryč! Hurá!
subject:
one: "Jedno nové oznámení od vaší poslední návštěvy \U0001F418"
other: "%{count} nových oznámení od vaší poslední návštěvy \U0001F418"
title: Ve vaší absenci... title: Ve vaší absenci...
favourite: favourite:
body: 'Váš příspěvek si oblíbil/a %{name}:' body: 'Váš příspěvek si oblíbil/a %{name}:'
@ -750,17 +730,11 @@ cs:
statuses: statuses:
attached: attached:
description: 'Přiloženo: %{attached}' description: 'Přiloženo: %{attached}'
image: image: "%{count} obrázků"
one: "%{count} obrázek" video: "%{count} videí"
other: "%{count} obrázků"
video:
one: "%{count} video"
other: "%{count} videí"
boosted_from_html: Boostnuto z %{acct_link} boosted_from_html: Boostnuto z %{acct_link}
content_warning: 'Varování o obsahu: %{warning}' content_warning: 'Varování o obsahu: %{warning}'
disallowed_hashtags: disallowed_hashtags: 'obsahuje nepovolené hashtagy: %{tags}'
one: 'obsahuje nepovolený hashtag: %{tags}'
other: 'obsahuje nepovolené hashtagy: %{tags}'
language_detection: Zjistit jazyk automaticky language_detection: Zjistit jazyk automaticky
open_in_web: Otevřít na webu open_in_web: Otevřít na webu
over_character_limit: limit %{max} znaků byl překročen over_character_limit: limit %{max} znaků byl překročen

View File

@ -30,22 +30,16 @@ cy:
other_instances: Rhestr achosion other_instances: Rhestr achosion
privacy_policy: Polisi preifatrwydd privacy_policy: Polisi preifatrwydd
source_code: Cod ffynhonnell source_code: Cod ffynhonnell
status_count_after: status_count_after: statws
one: statws
other: statws
status_count_before: Pwy ysgrifennodd status_count_before: Pwy ysgrifennodd
terms: Telerau gwasanaeth terms: Telerau gwasanaeth
user_count_after: user_count_after: defnyddwyr
one: defnyddiwr
other: defnyddwyr
user_count_before: Cartref i user_count_before: Cartref i
what_is_mastodon: Beth yw Mastodon? what_is_mastodon: Beth yw Mastodon?
accounts: accounts:
choices_html: 'Dewisiadau %{name}:' choices_html: 'Dewisiadau %{name}:'
follow: Dilynwch follow: Dilynwch
followers: followers: Dilynwyr
one: Dilynwr
other: Dilynwyr
following: Yn dilyn following: Yn dilyn
joined: Ymunodd %{date} joined: Ymunodd %{date}
media: Cyfryngau media: Cyfryngau
@ -56,9 +50,7 @@ cy:
people_who_follow: Pobl sy'n dilyn %{name} people_who_follow: Pobl sy'n dilyn %{name}
pin_errors: pin_errors:
following: Rhaid i ti fod yn dilyn y person yr ydych am ei gymeradwyo yn barod following: Rhaid i ti fod yn dilyn y person yr ydych am ei gymeradwyo yn barod
posts: posts: Tŵtiau
one: Tŵt
other: Tŵtiau
posts_tab_heading: Tŵtiau posts_tab_heading: Tŵtiau
posts_with_replies: Tŵtiau ac atebion posts_with_replies: Tŵtiau ac atebion
reserved_username: Mae'r enw defnyddior yn neilltuedig reserved_username: Mae'r enw defnyddior yn neilltuedig
@ -262,9 +254,7 @@ cy:
suspend: Atal suspend: Atal
severity: Difrifoldeb severity: Difrifoldeb
show: show:
affected_accounts: affected_accounts: "%{count} o gyfrifoedd yn y bas data wedi eu hefeithio"
one: Mae un cyfri yn y bas data wedi ei effeithio
other: "%{count} o gyfrifoedd yn y bas data wedi eu hefeithio"
retroactive: retroactive:
silence: Dad-dawelu pob cyfri presennol o'r parth hwn silence: Dad-dawelu pob cyfri presennol o'r parth hwn
suspend: Dad-atal pob cyfrif o'r parth hwn sy'n bodoli suspend: Dad-atal pob cyfrif o'r parth hwn sy'n bodoli
@ -508,9 +498,7 @@ cy:
generic: generic:
changes_saved_msg: Llwyddwyd i gadw y newidiadau! changes_saved_msg: Llwyddwyd i gadw y newidiadau!
save_changes: Cadw newidiadau save_changes: Cadw newidiadau
validation_errors: validation_errors: Mae rhywbeth o'i le o hyd! Edrychwch ar y %{count} gwall isod os gwelwch yn dda
one: Mae rhywbeth o'i le o hyd! Edrychwch ar y gwall isod os gwelwch yn dda
other: Mae rhywbeth o'i le o hyd! Edrychwch ar y %{count} gwall isod os gwelwch yn dda
imports: imports:
preface: Mae modd mewnforio data yr ydych wedi allforio o achos arall, megis rhestr o bobl yr ydych yn ei ddilyn neu yn blocio. preface: Mae modd mewnforio data yr ydych wedi allforio o achos arall, megis rhestr o bobl yr ydych yn ei ddilyn neu yn blocio.
success: Uwchlwyddwyd eich data yn llwyddiannus ac fe fydd yn cael ei brosesu mewn da bryd success: Uwchlwyddwyd eich data yn llwyddiannus ac fe fydd yn cael ei brosesu mewn da bryd

View File

@ -1 +1,2 @@
---
ast: {} ast: {}

View File

@ -77,6 +77,4 @@ cs:
expired: vypršel, prosím vyžádejte si nový expired: vypršel, prosím vyžádejte si nový
not_found: nenalezen not_found: nenalezen
not_locked: nebyl uzamčen not_locked: nebyl uzamčen
not_saved: not_saved: "%{count} chyb zabránila uložení tohoto %{resource}:"
one: '1 chyba zabránila uložení tohoto %{resource}:'
other: "%{count} chyb zabránila uložení tohoto %{resource}:"

View File

@ -77,6 +77,4 @@ cy:
expired: wedi dod i ben, gwnewch gais am un newydd os gwelwch yn dda expired: wedi dod i ben, gwnewch gais am un newydd os gwelwch yn dda
not_found: heb ei ganfod not_found: heb ei ganfod
not_locked: heb ei gloi not_locked: heb ei gloi
not_saved: not_saved: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd %{count} gwall:'
one: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd 1 gwall:'
other: 'Gwaharddwyd yr %{resource} rhag cael ei arbed oherwydd %{count} gwall:'

View File

@ -58,6 +58,4 @@ hr:
expired: je istekao, zatraži novu expired: je istekao, zatraži novu
not_found: nije nađen not_found: nije nađen
not_locked: nije zaključan not_locked: nije zaključan
not_saved: not_saved: "%{count} greške su zabranile da ovaj %{resource} bude sačuvan:"
one: '1 greška je zabranila da ovaj %{resource} bude sačuvan:'
other: "%{count} greške su zabranile da ovaj %{resource} bude sačuvan:"

View File

@ -77,6 +77,4 @@ pl:
expired: wygasło, poproś o nowe expired: wygasło, poproś o nowe
not_found: nie znaleziono not_found: nie znaleziono
not_locked: było zablokowane not_locked: było zablokowane
not_saved: not_saved: 'Błędy (%{count}) uniemożliwiły zapisanie zasobu %{resource}:'
one: '1 błąd uniemożliwił zapisanie zasobu %{resource}:'
other: 'Błędy (%{count}) uniemożliwiły zapisanie zasobu %{resource}:'

View File

@ -77,6 +77,4 @@ zh-TW:
expired: 已經過期,請重新申請 expired: 已經過期,請重新申請
not_found: 找不到 not_found: 找不到
not_locked: 並未被鎖定 not_locked: 並未被鎖定
not_saved: not_saved: "%{count} 個錯誤使 %{resource} 無法被儲存︰"
one: 1 個錯誤使 %{resource} 無法被儲存︰
other: "%{count} 個錯誤使 %{resource} 無法被儲存︰"

View File

@ -1 +1,2 @@
---
ast: {} ast: {}

View File

@ -1 +1,2 @@
---
{} {}

View File

@ -61,9 +61,7 @@ hr:
generic: generic:
changes_saved_msg: Izmjene su uspješno sačuvane! changes_saved_msg: Izmjene su uspješno sačuvane!
save_changes: Sačuvaj izmjene save_changes: Sačuvaj izmjene
validation_errors: validation_errors: Nešto još uvijek ne štima! Vidi %{count} greške ispod
one: Nešto ne štima! Vidi grešku ispod
other: Nešto još uvijek ne štima! Vidi %{count} greške ispod
imports: imports:
preface: Možeš uvesti određene podatke kao što su svi ljudi koje slijediš ili blokiraš u svoj račun na ovoj instanci, sa fajlova kreiranih izvozom sa druge instance. preface: Možeš uvesti određene podatke kao što su svi ljudi koje slijediš ili blokiraš u svoj račun na ovoj instanci, sa fajlova kreiranih izvozom sa druge instance.
success: Tvoji podaci su uspješno uploadani i bit će obrađeni u dogledno vrijeme success: Tvoji podaci su uspješno uploadani i bit će obrađeni u dogledno vrijeme
@ -76,12 +74,8 @@ hr:
digest: digest:
body: 'Ovo je kratak sažetak propuštenog od tvog prošlog posjeta %{since}:' body: 'Ovo je kratak sažetak propuštenog od tvog prošlog posjeta %{since}:'
mention: "%{name} te je spomenuo:" mention: "%{name} te je spomenuo:"
new_followers_summary: new_followers_summary: Imaš %{count} novih sljedbenika! Prekrašno!
one: Imaš novog sljedbenika! Yay! subject: "%{count} novih notifikacija od tvog prošlog posjeta \U0001F418"
other: Imaš %{count} novih sljedbenika! Prekrašno!
subject:
one: "1 nova notifikacija od tvog prošlog posjeta \U0001F418"
other: "%{count} novih notifikacija od tvog prošlog posjeta \U0001F418"
favourite: favourite:
body: 'Tvoj status je %{name} označio kao omiljen:' body: 'Tvoj status je %{name} označio kao omiljen:'
subject: "%{name} je označio kao omiljen tvoj status" subject: "%{name} je označio kao omiljen tvoj status"

View File

@ -279,10 +279,7 @@ pl:
suspend: Zawieś suspend: Zawieś
severity: Priorytet severity: Priorytet
show: show:
affected_accounts: affected_accounts: Dotyczy %{count} kont w bazie danych
many: Dotyczy %{count} kont w bazie danych
one: Dotyczy jednego konta w bazie danych
other: Dotyczy %{count} kont w bazie danych
retroactive: retroactive:
silence: Odwołaj wyciszenie wszystkich kont w tej domenie silence: Odwołaj wyciszenie wszystkich kont w tej domenie
suspend: Odwołaj zawieszenie wszystkich kont w tej domenie suspend: Odwołaj zawieszenie wszystkich kont w tej domenie
@ -577,9 +574,7 @@ pl:
followers_count: Liczba śledzących followers_count: Liczba śledzących
lock_link: Zablokuj swoje konto lock_link: Zablokuj swoje konto
purge: Przestań śledzić purge: Przestań śledzić
success: success: W trakcie usuwania śledzących z %{count} domen…
one: W trakcie usuwania śledzących z jednej domeny…
other: W trakcie usuwania śledzących z %{count} domen…
true_privacy_html: Pamiętaj, że <strong>rzeczywista prywatność może zostać uzyskana wyłącznie dzięki szyfrowaniu end-to-end</strong>. true_privacy_html: Pamiętaj, że <strong>rzeczywista prywatność może zostać uzyskana wyłącznie dzięki szyfrowaniu end-to-end</strong>.
unlocked_warning_html: Każdy może Cię śledzić, dzięki czemu może zobaczyć Twoje niepubliczne wpisy. %{lock_link} aby móc kontrolować, kto Cię śledzi. unlocked_warning_html: Każdy może Cię śledzić, dzięki czemu może zobaczyć Twoje niepubliczne wpisy. %{lock_link} aby móc kontrolować, kto Cię śledzi.
unlocked_warning_title: Twoje konto nie jest zablokowane unlocked_warning_title: Twoje konto nie jest zablokowane
@ -788,9 +783,7 @@ pl:
other: "%{count} filmów" other: "%{count} filmów"
boosted_from_html: Podbito przez %{acct_link} boosted_from_html: Podbito przez %{acct_link}
content_warning: 'Ostrzeżenie o zawartości: %{warning}' content_warning: 'Ostrzeżenie o zawartości: %{warning}'
disallowed_hashtags: disallowed_hashtags: 'zawiera niedozwolone hashtagi: %{tags}'
one: 'zawiera niedozwolony hashtag: %{tags}'
other: 'zawiera niedozwolone hashtagi: %{tags}'
language_detection: Automatycznie wykrywaj język language_detection: Automatycznie wykrywaj język
open_in_web: Otwórz w przeglądarce open_in_web: Otwórz w przeglądarce
over_character_limit: limit %{max} znaków przekroczony over_character_limit: limit %{max} znaków przekroczony

View File

@ -1 +1,2 @@
---
{} {}

View File

@ -30,22 +30,16 @@ sk:
other_instances: Zoznam ďalších inštancií other_instances: Zoznam ďalších inštancií
privacy_policy: Ustanovenia o súkromí privacy_policy: Ustanovenia o súkromí
source_code: Zdrojový kód source_code: Zdrojový kód
status_count_after: status_count_after: statusy
one: status
other: statusy
status_count_before: Ktorí napísali status_count_before: Ktorí napísali
terms: Podmienky užívania terms: Podmienky užívania
user_count_after: user_count_after: užívateľov
one: užívateľ
other: užívateľov
user_count_before: Domov pre user_count_before: Domov pre
what_is_mastodon: Čo je Mastodon? what_is_mastodon: Čo je Mastodon?
accounts: accounts:
choices_html: "%{name}vé voľby:" choices_html: "%{name}vé voľby:"
follow: Sledovať follow: Sledovať
followers: followers: Sledovatelia
one: Následovateľ
other: Sledovatelia
following: Sledovaní following: Sledovaní
joined: Pridal/a sa %{date} joined: Pridal/a sa %{date}
media: Médiá media: Médiá
@ -56,9 +50,7 @@ sk:
people_who_follow: Ľudia sledujúci %{name} people_who_follow: Ľudia sledujúci %{name}
pin_errors: pin_errors:
following: Musíš už následovať toho človeka, ktorého si praješ zviditeľniť following: Musíš už následovať toho človeka, ktorého si praješ zviditeľniť
posts: posts: Príspevky
one: Príspevok
other: Príspevky
posts_tab_heading: Príspevky posts_tab_heading: Príspevky
posts_with_replies: Príspevky s odpoveďami posts_with_replies: Príspevky s odpoveďami
reserved_username: Prihlasovacie meno je rezervované reserved_username: Prihlasovacie meno je rezervované
@ -746,9 +738,7 @@ sk:
other: "%{count} videí" other: "%{count} videí"
boosted_from_html: Povýšené od %{acct_link} boosted_from_html: Povýšené od %{acct_link}
content_warning: 'Varovanie o obsahu: %{warning}' content_warning: 'Varovanie o obsahu: %{warning}'
disallowed_hashtags: disallowed_hashtags: 'obsahuje nepovolené haštagy: %{tags}'
one: 'obsahuje nepovolený haštag: %{tags}'
other: 'obsahuje nepovolené haštagy: %{tags}'
language_detection: Zisti jazyk automaticky language_detection: Zisti jazyk automaticky
open_in_web: Otvor v okne prehliadača open_in_web: Otvor v okne prehliadača
over_character_limit: limit počtu %{max} znakov bol presiahnutý over_character_limit: limit počtu %{max} znakov bol presiahnutý

View File

@ -30,22 +30,16 @@ sr:
other_instances: Листа инстанци other_instances: Листа инстанци
privacy_policy: Полиса приватности privacy_policy: Полиса приватности
source_code: Изворни код source_code: Изворни код
status_count_after: status_count_after: статуси
one: статус
other: статуси
status_count_before: Који су написали status_count_before: Који су написали
terms: Услови коришћења terms: Услови коришћења
user_count_after: user_count_after: корисници
one: корисник
other: корисници
user_count_before: Дом за user_count_before: Дом за
what_is_mastodon: Шта је Мастодон? what_is_mastodon: Шта је Мастодон?
accounts: accounts:
choices_html: "%{name}'s избори:" choices_html: "%{name}'s избори:"
follow: Запрати follow: Запрати
followers: followers: Пратиоци
one: Пратилац
other: Пратиоци
following: Пратим following: Пратим
joined: Придружио/ла се %{date} joined: Придружио/ла се %{date}
media: Медији media: Медији
@ -56,9 +50,7 @@ sr:
people_who_follow: Људи који прате %{name} people_who_follow: Људи који прате %{name}
pin_errors: pin_errors:
following: Морате пратити ову особу ако хоћете да потврдите following: Морате пратити ову особу ако хоћете да потврдите
posts: posts: Трубе
one: Труба
other: Трубе
posts_tab_heading: Трубе posts_tab_heading: Трубе
posts_with_replies: Трубе и одговори posts_with_replies: Трубе и одговори
reserved_username: Корисничко име је резервисано reserved_username: Корисничко име је резервисано
@ -754,17 +746,11 @@ sr:
statuses: statuses:
attached: attached:
description: 'У прилогу: %{attached}' description: 'У прилогу: %{attached}'
image: image: "%{count} слике"
one: "%{count} слику" video: "%{count} видеа"
other: "%{count} слике"
video:
one: "%{count} видео"
other: "%{count} видеа"
boosted_from_html: Подржано од %{acct_link} boosted_from_html: Подржано од %{acct_link}
content_warning: 'Упозорење на садржај: %{warning}' content_warning: 'Упозорење на садржај: %{warning}'
disallowed_hashtags: disallowed_hashtags: 'садржи забрањене тарабе: %{tags}'
one: 'садржи забрањену тарабу: %{tags}'
other: 'садржи забрањене тарабе: %{tags}'
language_detection: Аутоматскo откривање језика language_detection: Аутоматскo откривање језика
open_in_web: Отвори у вебу open_in_web: Отвори у вебу
over_character_limit: ограничење од %{max} карактера прекорачено over_character_limit: ограничење од %{max} карактера прекорачено

View File

@ -518,18 +518,14 @@ uk:
followers_count: Кількість підписників followers_count: Кількість підписників
lock_link: Закрийте акаунт lock_link: Закрийте акаунт
purge: Видалити з підписників purge: Видалити з підписників
success: success: У процесі м'якого блокування підписників з %{count} доменів...
one: У процесі м'якого блокування підписників з одного домену...
other: У процесі м'якого блокування підписників з %{count} доменів...
true_privacy_html: Будь ласка, помітьте, що <strong>справжняя конфіденційність може бути досягнена тільки за допомогою end-to-end шифрування</strong>. true_privacy_html: Будь ласка, помітьте, що <strong>справжняя конфіденційність може бути досягнена тільки за допомогою end-to-end шифрування</strong>.
unlocked_warning_html: Хто завгодно може підписатися на Вас та отримати доступ до перегляду Ваших приватних статусів. %{lock_link}, щоб отримати можливість роздивлятися та вручну підтверджувати запити щодо підписки. unlocked_warning_html: Хто завгодно може підписатися на Вас та отримати доступ до перегляду Ваших приватних статусів. %{lock_link}, щоб отримати можливість роздивлятися та вручну підтверджувати запити щодо підписки.
unlocked_warning_title: Ваш аккаунт не закритий для підписки unlocked_warning_title: Ваш аккаунт не закритий для підписки
generic: generic:
changes_saved_msg: Зміни успішно збережені! changes_saved_msg: Зміни успішно збережені!
save_changes: Зберегти зміни save_changes: Зберегти зміни
validation_errors: validation_errors: Щось тут не так! Будь ласка, ознайомтеся з %{count} помилками нижче
one: Щось тут не так! Будь ласка, ознайомтеся з помилкою нижче
other: Щось тут не так! Будь ласка, ознайомтеся з %{count} помилками нижче
imports: imports:
preface: Вы можете завантажити деякі дані, наприклад, списки людей, на яких Ви підписані чи яких блокуєте, в Ваш акаунт на цій інстанції з файлів, експортованих з іншої інстанції. preface: Вы можете завантажити деякі дані, наприклад, списки людей, на яких Ви підписані чи яких блокуєте, в Ваш акаунт на цій інстанції з файлів, експортованих з іншої інстанції.
success: Ваші дані були успішно загружені та будуть оброблені в найближчий момент success: Ваші дані були успішно загружені та будуть оброблені в найближчий момент
@ -552,9 +548,7 @@ uk:
expires_in_prompt: Ніколи expires_in_prompt: Ніколи
generate: Згенерувати generate: Згенерувати
invited_by: 'Вас запросив(-ла):' invited_by: 'Вас запросив(-ла):'
max_uses: max_uses: "%{count} використань"
one: 1 використання
other: "%{count} використань"
max_uses_prompt: Без обмеження max_uses_prompt: Без обмеження
prompt: Генеруйте та діліться посиланням з іншими для надання доступу до сайту prompt: Генеруйте та діліться посиланням з іншими для надання доступу до сайту
table: table:
@ -703,17 +697,11 @@ uk:
statuses: statuses:
attached: attached:
description: 'Прикріплено: %{attached}' description: 'Прикріплено: %{attached}'
image: image: "%{count} картинки"
one: "%{count} картинка" video: "%{count} відео"
other: "%{count} картинки"
video:
one: "%{count} відео"
other: "%{count} відео"
boosted_from_html: Просунуто від %{acct_link} boosted_from_html: Просунуто від %{acct_link}
content_warning: 'Попередження про контент: %{warning}' content_warning: 'Попередження про контент: %{warning}'
disallowed_hashtags: disallowed_hashtags: 'містив заборонені хештеґи: %{tags}'
one: 'містив заборонений хештеґ: %{tags}'
other: 'містив заборонені хештеґи: %{tags}'
language_detection: Автоматично визначати мову language_detection: Автоматично визначати мову
open_in_web: Відкрити у вебі open_in_web: Відкрити у вебі
over_character_limit: перевищено ліміт символів (%{max}) over_character_limit: перевищено ліміт символів (%{max})

View File

@ -30,13 +30,10 @@ zh-CN:
other_instances: 其他实例 other_instances: 其他实例
privacy_policy: 隐私政策 privacy_policy: 隐私政策
source_code: 源代码 source_code: 源代码
status_count_after: status_count_after: 条嘟文
one: 条嘟文
status_count_before: 他们共嘟出了 status_count_before: 他们共嘟出了
terms: 使用条款 terms: 使用条款
user_count_after: user_count_after: 位用户
one: 位用户
other: 位用户
user_count_before: 这里共注册有 user_count_before: 这里共注册有
what_is_mastodon: Mastodon 是什么? what_is_mastodon: Mastodon 是什么?
accounts: accounts:

View File

@ -29,18 +29,15 @@ zh-TW:
learn_more: 了解詳細 learn_more: 了解詳細
other_instances: 其他站點 other_instances: 其他站點
source_code: 原始碼 source_code: 原始碼
status_count_after: status_count_after: 狀態
one: 狀態
status_count_before: 他們共嘟出了 status_count_before: 他們共嘟出了
terms: 使用條款 terms: 使用條款
user_count_after: user_count_after: 使用者
one: 使用者
user_count_before: 這裡共註冊有 user_count_before: 這裡共註冊有
what_is_mastodon: 什麼是 Mastodon? what_is_mastodon: 什麼是 Mastodon?
accounts: accounts:
follow: 關注 follow: 關注
followers: followers: 關注者
other: 關注者
following: 正在關注 following: 正在關注
media: 媒體 media: 媒體
moved_html: "%{name} 已經搬遷到 %{new_profile_link}:" moved_html: "%{name} 已經搬遷到 %{new_profile_link}:"
@ -48,9 +45,7 @@ zh-TW:
nothing_here: 暫時沒有內容可供顯示! nothing_here: 暫時沒有內容可供顯示!
people_followed_by: "%{name} 關注的人" people_followed_by: "%{name} 關注的人"
people_who_follow: 關注 %{name} 的人 people_who_follow: 關注 %{name} 的人
posts: posts: 嘟文
one: 嘟掉
other: 嘟文
posts_tab_heading: 嘟文 posts_tab_heading: 嘟文
posts_with_replies: 嘟文與回覆 posts_with_replies: 嘟文與回覆
reserved_username: 此用戶名已被保留 reserved_username: 此用戶名已被保留
@ -234,9 +229,7 @@ zh-TW:
suspend: 自動封鎖 suspend: 自動封鎖
severity: 嚴重度 severity: 嚴重度
show: show:
affected_accounts: affected_accounts: 資料庫中有%{count}個使用者受影響
one: 資料庫中有一個使用者受到影響
other: 資料庫中有%{count}個使用者受影響
retroactive: retroactive:
silence: 對此網域的所有使用者取消靜音 silence: 對此網域的所有使用者取消靜音
suspend: 對此網域的所有使用者取消封鎖 suspend: 對此網域的所有使用者取消封鎖
@ -480,18 +473,14 @@ zh-TW:
followers_count: 關注者數量 followers_count: 關注者數量
lock_link: 將你的帳戶設定為私人 lock_link: 將你的帳戶設定為私人
purge: 移除關注者 purge: 移除關注者
success: success: 正準備軟性封鎖 %{count} 個網域的關注者……
one: 正準備軟性封鎖 1 個網域的關注者……
other: 正準備軟性封鎖 %{count} 個網域的關注者……
true_privacy_html: 請謹記,唯有<strong>點對點加密方可以真正確保你的隱私</strong>。 true_privacy_html: 請謹記,唯有<strong>點對點加密方可以真正確保你的隱私</strong>。
unlocked_warning_html: 任何人都可以在關注你後立即查看非公開的嘟文。只要%{lock_link},你就可以審核並拒絕關注請求。 unlocked_warning_html: 任何人都可以在關注你後立即查看非公開的嘟文。只要%{lock_link},你就可以審核並拒絕關注請求。
unlocked_warning_title: 你的帳戶是公開的 unlocked_warning_title: 你的帳戶是公開的
generic: generic:
changes_saved_msg: 已成功儲存修改! changes_saved_msg: 已成功儲存修改!
save_changes: 儲存修改 save_changes: 儲存修改
validation_errors: validation_errors: 送出的資料有 %{count} 個問題
one: 送出的資料有問題
other: 送出的資料有 %{count} 個問題
imports: imports:
preface: 您可以在此匯入您在其他站點所匯出的資料檔,包括關注的使用者、封鎖的使用者名單。 preface: 您可以在此匯入您在其他站點所匯出的資料檔,包括關注的使用者、封鎖的使用者名單。
success: 資料檔上傳成功,正在匯入,請稍候 success: 資料檔上傳成功,正在匯入,請稍候
@ -514,9 +503,7 @@ zh-TW:
expires_in_prompt: 永不過期 expires_in_prompt: 永不過期
generate: 建立邀請連結 generate: 建立邀請連結
invited_by: 你的邀請人是: invited_by: 你的邀請人是:
max_uses: max_uses: "%{count} 次"
one: 1
other: "%{count} 次"
max_uses_prompt: 無限制 max_uses_prompt: 無限制
prompt: 建立分享連結,邀請他人在本站點註冊 prompt: 建立分享連結,邀請他人在本站點註冊
table: table:
@ -542,12 +529,8 @@ zh-TW:
action: 閱覽所有通知 action: 閱覽所有通知
body: 以下是自%{since}你最後一次登入以來錯過的訊息摘要 body: 以下是自%{since}你最後一次登入以來錯過的訊息摘要
mention: "%{name} 在此提及了你:" mention: "%{name} 在此提及了你:"
new_followers_summary: new_followers_summary: 而且,你不在的時候,有 %{count} 個人關注你了! 好棒!
one: 而且,你不在的時候,有一個人關注你! 耶! subject: "自從上次登入以來,你收到 %{count} 則新的通知 \U0001F418"
other: 而且,你不在的時候,有 %{count} 個人關注你了! 好棒!
subject:
one: "自從上次登入以來,你收到 1 則新的通知 \U0001F418"
other: "自從上次登入以來,你收到 %{count} 則新的通知 \U0001F418"
title: 你不在的時候... title: 你不在的時候...
favourite: favourite:
body: '你的嘟文被 %{name} 加入了最愛:' body: '你的嘟文被 %{name} 加入了最愛:'
@ -653,17 +636,11 @@ zh-TW:
statuses: statuses:
attached: attached:
description: 附件: %{attached} description: 附件: %{attached}
image: image: "%{count} 幅圖片"
one: "%{count} 幅圖片" video: "%{count} 段影片"
other: "%{count} 幅圖片"
video:
one: "%{count} 段影片"
other: "%{count} 段影片"
boosted_from_html: 轉嘟自 %{acct_link} boosted_from_html: 轉嘟自 %{acct_link}
content_warning: 內容警告: %{warning} content_warning: 內容警告: %{warning}
disallowed_hashtags: disallowed_hashtags: 包含不允許的標籤: %{tags}
one: 包含不允許的標籤: %{tags}
other: 包含不允許的標籤: %{tags}
language_detection: 自動偵測語言 language_detection: 自動偵測語言
open_in_web: 以網頁開啟 open_in_web: 以網頁開啟
over_character_limit: 超過了 %{max} 字的限制 over_character_limit: 超過了 %{max} 字的限制

View File

@ -14,12 +14,29 @@ class MigrateAccountConversations < ActiveRecord::Migration[5.2]
sleep 1 sleep 1
end end
local_direct_statuses.find_each do |status| total = estimate_rows(local_direct_statuses) + estimate_rows(notifications_about_direct_statuses)
migrated = 0
started_time = Time.zone.now
last_time = Time.zone.now
local_direct_statuses.includes(:account, mentions: :account).find_each do |status|
AccountConversation.add_status(status.account, status) AccountConversation.add_status(status.account, status)
migrated += 1
if Time.zone.now - last_time > 1
say_progress(migrated, total, started_time)
last_time = Time.zone.now
end
end end
notifications_about_direct_statuses.find_each do |notification| notifications_about_direct_statuses.includes(:account, mention: { status: [:account, mentions: :account] }).find_each do |notification|
AccountConversation.add_status(notification.account, notification.target_status) AccountConversation.add_status(notification.account, notification.target_status)
migrated += 1
if Time.zone.now - last_time > 1
say_progress(migrated, total, started_time)
last_time = Time.zone.now
end
end end
end end
@ -28,16 +45,31 @@ class MigrateAccountConversations < ActiveRecord::Migration[5.2]
private private
def estimate_rows(query)
result = exec_query("EXPLAIN #{query.to_sql}").first
result['QUERY PLAN'].scan(/ rows=([\d]+)/).first&.first&.to_i || 0
end
def say_progress(migrated, total, started_time)
status = "Migrated #{migrated} rows"
percentage = 100.0 * migrated / total
status += " (~#{sprintf('%.2f', percentage)}%, "
remaining_time = (100.0 - percentage) * (Time.zone.now - started_time) / percentage
status += "#{(remaining_time / 60).to_i}:"
status += sprintf('%02d', remaining_time.to_i % 60)
status += ' remaining)'
say status, true
end
def local_direct_statuses def local_direct_statuses
Status.unscoped Status.unscoped.local.where(visibility: :direct)
.local
.where(visibility: :direct)
.includes(:account, mentions: :account)
end end
def notifications_about_direct_statuses def notifications_about_direct_statuses
Notification.joins(mention: :status) Notification.joins(mention: :status).where(activity_type: 'Mention', statuses: { visibility: :direct })
.where(activity_type: 'Mention', statuses: { visibility: :direct })
.includes(:account, mention: { status: [:account, mentions: :account] })
end end
end end

View File

@ -6,6 +6,7 @@ require_relative 'mastodon/emoji_cli'
require_relative 'mastodon/accounts_cli' require_relative 'mastodon/accounts_cli'
require_relative 'mastodon/feeds_cli' require_relative 'mastodon/feeds_cli'
require_relative 'mastodon/settings_cli' require_relative 'mastodon/settings_cli'
require_relative 'mastodon/domains_cli'
module Mastodon module Mastodon
class CLI < Thor class CLI < Thor
@ -27,5 +28,8 @@ module Mastodon
desc 'settings SUBCOMMAND ...ARGS', 'Manage dynamic settings' desc 'settings SUBCOMMAND ...ARGS', 'Manage dynamic settings'
subcommand 'settings', Mastodon::SettingsCLI subcommand 'settings', Mastodon::SettingsCLI
desc 'domains SUBCOMMAND ...ARGS', 'Manage account domains'
subcommand 'domains', Mastodon::DomainsCLI
end end
end end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'rubygems/package' require 'set'
require_relative '../../config/boot' require_relative '../../config/boot'
require_relative '../../config/environment' require_relative '../../config/environment'
require_relative 'cli_helper' require_relative 'cli_helper'
@ -10,6 +10,7 @@ module Mastodon
def self.exit_on_failure? def self.exit_on_failure?
true true
end end
option :all, type: :boolean option :all, type: :boolean
desc 'rotate [USERNAME]', 'Generate and broadcast new keys' desc 'rotate [USERNAME]', 'Generate and broadcast new keys'
long_desc <<-LONG_DESC long_desc <<-LONG_DESC
@ -210,33 +211,25 @@ module Mastodon
Accounts that have had confirmed activity within the last week Accounts that have had confirmed activity within the last week
are excluded from the checks. are excluded from the checks.
If 10 or more accounts from the same domain cannot be queried Domains that are unreachable are not checked.
due to a connection error (such as missing DNS records) then
the domain is considered dead, and all other accounts from it
are deleted without further querying.
With the --dry-run option, no deletes will actually be carried With the --dry-run option, no deletes will actually be carried
out. out.
LONG_DESC LONG_DESC
def cull def cull
domain_thresholds = Hash.new { |hash, key| hash[key] = 0 } skip_threshold = 7.days.ago
skip_threshold = 7.days.ago culled = 0
culled = 0 skip_domains = Set.new
dead_servers = [] dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
Account.remote.where(protocol: :activitypub).partitioned.find_each do |account| Account.remote.where(protocol: :activitypub).partitioned.find_each do |account|
next if account.updated_at >= skip_threshold || (account.last_webfingered_at.present? && account.last_webfingered_at >= skip_threshold) next if account.updated_at >= skip_threshold || (account.last_webfingered_at.present? && account.last_webfingered_at >= skip_threshold)
unless dead_servers.include?(account.domain) unless skip_domains.include?(account.domain)
begin begin
code = Request.new(:head, account.uri).perform(&:code) code = Request.new(:head, account.uri).perform(&:code)
rescue HTTP::ConnectionError rescue HTTP::ConnectionError
domain_thresholds[account.domain] += 1 skip_domains << account.domain
if domain_thresholds[account.domain] >= 10
dead_servers << account.domain
end
rescue StandardError rescue StandardError
next next
end end
@ -255,24 +248,12 @@ module Mastodon
end end
end end
# Remove dead servers
unless dead_servers.empty? || options[:dry_run]
dead_servers.each do |domain|
Account.where(domain: domain).find_each do |account|
SuspendAccountService.new.call(account)
account.destroy
culled += 1
say('.', :green, false)
end
end
end
say say
say("Removed #{culled} accounts (#{dead_servers.size} dead servers)#{dry_run}", :green) say("Removed #{culled} accounts. #{skip_domains.size} servers skipped#{dry_run}", skip_domains.empty? ? :green : :yellow)
unless dead_servers.empty? unless skip_domains.empty?
say('R.I.P.:', :yellow) say('The following servers were not available during the check:', :yellow)
dead_servers.each { |domain| say(' ' + domain) } skip_domains.each { |domain| say(' ' + domain) }
end end
end end

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
require_relative '../../config/boot'
require_relative '../../config/environment'
require_relative 'cli_helper'
module Mastodon
class DomainsCLI < Thor
def self.exit_on_failure?
true
end
option :dry_run, type: :boolean
desc 'purge DOMAIN', 'Remove accounts from a DOMAIN without a trace'
long_desc <<-LONG_DESC
Remove all accounts from a given DOMAIN without leaving behind any
records. Unlike a suspension, if the DOMAIN still exists in the wild,
it means the accounts could return if they are resolved again.
LONG_DESC
def purge(domain)
removed = 0
dry_run = options[:dry_run] ? ' (DRY RUN)' : ''
Account.where(domain: domain).find_each do |account|
unless options[:dry_run]
SuspendAccountService.new.call(account)
account.destroy
end
removed += 1
say('.', :green, false)
end
DomainBlock.where(domain: domain).destroy_all
say
say("Removed #{removed} accounts#{dry_run}", :green)
end
end
end

View File

@ -10,6 +10,7 @@ module Mastodon
def self.exit_on_failure? def self.exit_on_failure?
true true
end end
option :prefix option :prefix
option :suffix option :suffix
option :overwrite, type: :boolean option :overwrite, type: :boolean

View File

@ -9,6 +9,7 @@ module Mastodon
def self.exit_on_failure? def self.exit_on_failure?
true true
end end
option :all, type: :boolean, default: false option :all, type: :boolean, default: false
option :background, type: :boolean, default: false option :background, type: :boolean, default: false
option :dry_run, type: :boolean, default: false option :dry_run, type: :boolean, default: false
@ -58,7 +59,7 @@ module Mastodon
account = Account.find_local(username) account = Account.find_local(username)
if account.nil? if account.nil?
say("Account #{username} is not found", :red) say('No such account', :red)
exit(1) exit(1)
end end

View File

@ -9,6 +9,7 @@ module Mastodon
def self.exit_on_failure? def self.exit_on_failure?
true true
end end
option :days, type: :numeric, default: 7 option :days, type: :numeric, default: 7
option :background, type: :boolean, default: false option :background, type: :boolean, default: false
option :verbose, type: :boolean, default: false option :verbose, type: :boolean, default: false

View File

@ -9,6 +9,7 @@ module Mastodon
def self.exit_on_failure? def self.exit_on_failure?
true true
end end
desc 'open', 'Open registrations' desc 'open', 'Open registrations'
def open def open
Setting.open_registrations = true Setting.open_registrations = true