Compare commits

...

13 Commits

Author SHA1 Message Date
kibigo!
ac686d5a5d Fixed overflow issue 2018-01-05 13:34:21 -08:00
kibigo!
ec620ae486 Styling fixes 2018-01-05 12:41:15 -08:00
Nolan Darilek
3b016342c6 Fix accessibility of column headers
As a screen reader user new to Mastodon, I encountered the following issues with the column headers as designed:
 * Jumping between them was difficult. FOr instance, passing my home timeline to reach notification settings was difficult to impossible, especially considering infinite scrolling.
 * There doesn't appear to be any means for triggering the control via the keyboard. the `titleClick` handler only responds to mouse clicks.
 * I didn't even realize there was a Settings toggle until I made this change.

Thanks for using ARIA in your designs. It's a huge help. But adding a `button` role doesn't add keyboard handling and other button behavior. Also, because the role was on the heading container, it obscured the controls within the container itself. This fix resolve that. It also exposes the headings as headings rather than buttons, enabling skipping columns by using screen readers' heading navigation commands.

Since I myself am blind, if this fix requires additional visual styling, I'd like help applying that so it can be merged. I'd consider it an essential accessibility fix for my and other blind users' existence on the platform. Thanks!
2018-01-04 10:25:26 -06:00
Yamagishi Kazutoshi
3c18964256 Fallback default thumbnail in instance status API (#6177) 2018-01-04 15:36:55 +01:00
Marcin Mikołajczak
c61dd918a2 i18n: Update Polish translation (#6176)
Signed-off-by: Marcin Mikołajczak <me@m4sk.in>
2018-01-04 23:15:29 +09:00
Eugen Rochko
02ba03d6db Send one Delete of Actor in ActivityPub when account is suspended (#6172) 2018-01-04 14:40:49 +01:00
ThibG
3bee0996c5 Make sure private toots remain private and do not end up in HTTP caches (#6175) 2018-01-04 14:39:38 +01:00
muan
89daeb43a8 Improve Traditional Chinese translation (#6166)
* Improve Traditional Chinese translations

* Sort alphabetically
2018-01-04 05:00:50 +01:00
Eugen Rochko
7d4f4f9aab Fix FetchAtomService not finding alternatives if there's a Link header (#6170)
without them, such as is the case with GNU social

Fixes the ability to find GNU social accounts via URL in search and
when using remote follow function
2018-01-04 04:56:04 +01:00
Akihiko Odaki
256c2b1de0 Rearrange items in Getting Started navigation (#6126)
Though the subsections are representing features such as navigation and
settings, they are categorized by the ways how they are implemented
(internal navigation or external links.) They are irrelevant and some
arrangements were confusing because of that. (It is nonsense that instance
information is in settings subsection, for example.)

This fixes the issue by rearranging.
2018-01-04 10:56:54 +09:00
Eugen Rochko
02e3e1ec09 Fix nil error in log_target_from_history helper (#6173) 2018-01-04 10:56:23 +09:00
Eugen Rochko
ff924f95bb Fix OpenSSL dependency in ostatus2 (#6174) 2018-01-04 10:56:00 +09:00
Eugen Rochko
c10f4bdb03 Cache JSON of immutable ActivityPub representations (#6171) 2018-01-04 01:21:38 +01:00
20 changed files with 181 additions and 119 deletions

View File

@@ -299,13 +299,11 @@ GEM
sidekiq (>= 3.5.0)
statsd-ruby (~> 1.2.0)
oj (3.3.9)
openssl (2.0.6)
orm_adapter (0.5.0)
ostatus2 (2.0.1)
ostatus2 (2.0.2)
addressable (~> 2.4)
http (~> 2.0)
nokogiri (~> 1.6)
openssl (~> 2.0)
ox (2.8.2)
paperclip (5.1.0)
activemodel (>= 4.2.0)

View File

@@ -2,7 +2,8 @@
class AccountsController < ApplicationController
include AccountControllerConcern
include SignatureVerification
before_action :set_cache_headers
def show
respond_to do |format|
@@ -26,10 +27,11 @@ class AccountsController < ApplicationController
end
format.json do
render json: @account,
serializer: ActivityPub::ActorSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
skip_session!
render_cached_json(['activitypub', 'actor', @account.cache_key], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@account, serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter)
end
end
end
end

View File

@@ -4,15 +4,19 @@ class ActivityPub::FollowsController < Api::BaseController
include SignatureVerification
def show
render(
json: FollowRequest.includes(:account).references(:account).find_by!(
id: params.require(:id),
accounts: { domain: nil, username: params.require(:account_username) },
target_account: signed_request_account
),
serializer: ActivityPub::FollowSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
render json: follow_request,
serializer: ActivityPub::FollowSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
end
private
def follow_request
FollowRequest.includes(:account).references(:account).find_by!(
id: params.require(:id),
accounts: { domain: nil, username: params.require(:account_username) },
target_account: signed_request_account
)
end
end

View File

@@ -123,11 +123,24 @@ class ApplicationController < ActionController::Base
end
def render_cached_json(cache_key, **options)
options[:expires_in] ||= 3.minutes
options[:public] ||= true
cache_key = cache_key.join(':') if cache_key.is_a?(Enumerable)
content_type = options.delete(:content_type) || 'application/json'
data = Rails.cache.fetch(cache_key, { raw: true }.merge(options)) do
yield.to_json
end
expires_in options[:expires_in], public: true
render json: data
expires_in options[:expires_in], public: options[:public]
render json: data, content_type: content_type
end
def set_cache_headers
response.headers['Vary'] = 'Accept'
end
def skip_session!
request.session_options[:skip] = true
end
end

View File

@@ -2,14 +2,16 @@
class EmojisController < ApplicationController
before_action :set_emoji
before_action :set_cache_headers
def show
respond_to do |format|
format.json do
render json: @emoji,
serializer: ActivityPub::EmojiSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
skip_session!
render_cached_json(['activitypub', 'emoji', @emoji.cache_key], content_type: 'application/activity+json') do
ActiveModelSerializers::SerializableResource.new(@emoji, serializer: ActivityPub::EmojiSerializer, adapter: ActivityPub::Adapter)
end
end
end
end

View File

@@ -10,7 +10,7 @@ class StatusesController < ApplicationController
before_action :set_link_headers
before_action :check_account_suspension
before_action :redirect_to_original, only: [:show]
before_action { response.headers['Vary'] = 'Accept' }
before_action :set_cache_headers
def show
respond_to do |format|
@@ -22,25 +22,21 @@ class StatusesController < ApplicationController
end
format.json do
render json: @status,
serializer: ActivityPub::NoteSerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
skip_session! unless @stream_entry.hidden?
# Allow HTTP caching for 3 minutes if the status is public
unless @stream_entry.hidden?
request.session_options[:skip] = true
expires_in(3.minutes, public: true)
render_cached_json(['activitypub', 'note', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter)
end
end
end
end
def activity
render json: @status,
serializer: ActivityPub::ActivitySerializer,
adapter: ActivityPub::Adapter,
content_type: 'application/activity+json'
skip_session!
render_cached_json(['activitypub', 'activity', @status.cache_key], content_type: 'application/activity+json', public: !@stream_entry.hidden?) do
ActiveModelSerializers::SerializableResource.new(@status, serializer: ActivityPub::ActivitySerializer, adapter: ActivityPub::Adapter)
end
end
def embed

View File

@@ -34,7 +34,7 @@ module Admin::ActionLogsHelper
link_to attributes['domain'], "https://#{attributes['domain']}"
when 'Status'
tmp_status = Status.new(attributes)
link_to tmp_status.account.acct, TagManager.instance.url_for(tmp_status)
link_to tmp_status.account&.acct || "##{tmp_status.account_id}", TagManager.instance.url_for(tmp_status)
end
end

View File

@@ -4,6 +4,7 @@ module RoutingHelper
extend ActiveSupport::Concern
include Rails.application.routes.url_helpers
include ActionView::Helpers::AssetTagHelper
include Webpacker::Helper
included do
def default_url_options
@@ -17,6 +18,10 @@ module RoutingHelper
URI.join(root_url, source).to_s
end
def full_pack_url(source, **options)
full_asset_url(asset_pack_path(source, options))
end
private
def use_storage?

View File

@@ -23,7 +23,6 @@ export default class ColumnHeader extends React.PureComponent {
icon: PropTypes.string.isRequired,
active: PropTypes.bool,
multiColumn: PropTypes.bool,
focusable: PropTypes.bool,
showBackButton: PropTypes.bool,
children: PropTypes.node,
pinned: PropTypes.bool,
@@ -32,10 +31,6 @@ export default class ColumnHeader extends React.PureComponent {
onClick: PropTypes.func,
};
static defaultProps = {
focusable: true,
}
state = {
collapsed: true,
animating: false,
@@ -68,7 +63,7 @@ export default class ColumnHeader extends React.PureComponent {
}
render () {
const { title, icon, active, children, pinned, onPin, multiColumn, focusable, showBackButton, intl: { formatMessage } } = this.props;
const { title, icon, active, children, pinned, onPin, multiColumn, showBackButton, intl: { formatMessage } } = this.props;
const { collapsed, animating } = this.state;
const wrapperClassName = classNames('column-header__wrapper', {
@@ -135,11 +130,13 @@ export default class ColumnHeader extends React.PureComponent {
return (
<div className={wrapperClassName}>
<h1 tabIndex={focusable ? 0 : null} role='button' className={buttonClassName} aria-label={title} onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
<span className='column-header__title'>
{title}
</span>
<h1 className={buttonClassName}>
<button onClick={this.handleTitleClick}>
<i className={`fa fa-fw fa-${icon} column-header__icon`} />
<span className='column-header__title'>
{title}
</span>
</button>
<div className='column-header__buttons'>
{backButton}

View File

@@ -70,30 +70,28 @@ export default class GettingStarted extends ImmutablePureComponent {
navItems.push(
<ColumnLink key='4' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='5' icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />,
<ColumnLink key='6' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
<ColumnLink key='5' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
);
if (myAccount.get('locked')) {
navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
navItems.push(<ColumnLink key='6' icon='users' text={intl.formatMessage(messages.follow_requests)} to='/follow_requests' />);
}
navItems.push(
<ColumnLink key='8' icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />,
<ColumnLink key='9' icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
);
if (multiColumn) {
navItems.push(<ColumnLink key='10' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
navItems.push(<ColumnLink key='7' icon='question' text={intl.formatMessage(messages.keyboard_shortcuts)} to='/keyboard-shortcuts' />);
}
navItems.push(<ColumnLink key='8' icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />);
return (
<Column icon='asterisk' heading={intl.formatMessage(messages.heading)} hideHeadingOnMobile>
<div className='getting-started__wrapper'>
<ColumnSubheading text={intl.formatMessage(messages.navigation_subheading)} />
{navItems}
<ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} />
<ColumnLink icon='book' text={intl.formatMessage(messages.info)} href='/about/more' />
<ColumnLink icon='thumb-tack' text={intl.formatMessage(messages.pins)} to='/pinned' />
<ColumnLink icon='volume-off' text={intl.formatMessage(messages.mutes)} to='/mutes' />
<ColumnLink icon='ban' text={intl.formatMessage(messages.blocks)} to='/blocks' />
<ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />
<ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' />
</div>

View File

@@ -64,8 +64,8 @@
"confirmations.block.message": "你確定要封鎖 {name} ",
"confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除這個狀態?",
"confirmations.delete_list.confirm": "Delete",
"confirmations.delete_list.message": "Are you sure you want to permanently delete this list?",
"confirmations.delete_list.confirm": "刪除",
"confirmations.delete_list.message": "確定要永久性地刪除這個名單嗎?",
"confirmations.domain_block.confirm": "隱藏整個網域",
"confirmations.domain_block.message": "你真的真的確定要隱藏整個 {domain} ?多數情況下,比較推薦封鎖或消音幾個特定目標就好。",
"confirmations.mute.confirm": "消音",
@@ -128,14 +128,14 @@
"lightbox.close": "關閉",
"lightbox.next": "繼續",
"lightbox.previous": "回退",
"lists.account.add": "Add to list",
"lists.account.remove": "Remove from list",
"lists.delete": "Delete list",
"lists.edit": "Edit list",
"lists.new.create": "Add list",
"lists.new.title_placeholder": "New list title",
"lists.search": "Search among people you follow",
"lists.subheading": "Your lists",
"lists.account.add": "加到名單裡",
"lists.account.remove": "從名單中移除",
"lists.delete": "刪除名單",
"lists.edit": "修改名單",
"lists.new.create": "新增名單",
"lists.new.title_placeholder": "名單名稱",
"lists.search": "搜尋您關注的使用者",
"lists.subheading": "您的名單",
"loading_indicator.label": "讀取中...",
"media_gallery.toggle_visible": "切換可見性",
"missing_indicator.label": "找不到",
@@ -146,8 +146,8 @@
"navigation_bar.favourites": "最愛",
"navigation_bar.follow_requests": "關注請求",
"navigation_bar.info": "關於本站",
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
"navigation_bar.lists": "Lists",
"navigation_bar.keyboard_shortcuts": "快速鍵",
"navigation_bar.lists": "名單",
"navigation_bar.logout": "登出",
"navigation_bar.mutes": "消音的使用者",
"navigation_bar.pins": "置頂貼文",

View File

@@ -2350,6 +2350,19 @@
position: relative;
z-index: 2;
outline: 0;
overflow: hidden;
& > button {
display: flex;
flex: auto;
margin: 0;
border: none;
padding: 0;
color: inherit;
background: transparent;
font: inherit;
text-align: left;
}
&.active {
box-shadow: 0 1px 0 rgba($ui-highlight-color, 0.3);

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
class ActivityPub::DeleteActorSerializer < ActiveModel::Serializer
attributes :id, :type, :actor
attribute :virtual_object, key: :object
def id
[ActivityPub::TagManager.instance.uri_for(object), '#delete'].join
end
def type
'Delete'
end
def actor
ActivityPub::TagManager.instance.uri_for(object)
end
def virtual_object
actor
end
end

View File

@@ -27,7 +27,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer
end
def thumbnail
full_asset_url(instance_presenter.thumbnail.file.url) if instance_presenter.thumbnail
instance_presenter.thumbnail ? full_asset_url(instance_presenter.thumbnail.file.url) : full_pack_url('preview.jpg')
end
def stats

View File

@@ -17,9 +17,7 @@ class BatchedRemoveStatusService < BaseService
@stream_entry_batches = []
@salmon_batches = []
@activity_json_batches = []
@json_payloads = statuses.map { |s| [s.id, Oj.dump(event: :delete, payload: s.id.to_s)] }.to_h
@activity_json = {}
@activity_xml = {}
# Ensure that rendered XML reflects destroyed state
@@ -32,10 +30,7 @@ class BatchedRemoveStatusService < BaseService
unpush_from_home_timelines(account, account_statuses)
unpush_from_list_timelines(account, account_statuses)
if account.local?
batch_stream_entries(account, account_statuses)
batch_activity_json(account, account_statuses)
end
batch_stream_entries(account, account_statuses) if account.local?
end
# Cannot be batched
@@ -46,7 +41,6 @@ class BatchedRemoveStatusService < BaseService
Pubsubhubbub::RawDistributionWorker.push_bulk(@stream_entry_batches) { |batch| batch }
NotificationWorker.push_bulk(@salmon_batches) { |batch| batch }
ActivityPub::DeliveryWorker.push_bulk(@activity_json_batches) { |batch| batch }
end
private
@@ -57,22 +51,6 @@ class BatchedRemoveStatusService < BaseService
end
end
def batch_activity_json(account, statuses)
account.followers.inboxes.each do |inbox_url|
statuses.each do |status|
@activity_json_batches << [build_json(status), account.id, inbox_url]
end
end
statuses.each do |status|
other_recipients = (status.mentions + status.reblogs).map(&:account).reject(&:local?).select(&:activitypub?).uniq(&:id)
other_recipients.each do |target_account|
@activity_json_batches << [build_json(status), account.id, target_account.inbox_url]
end
end
end
def unpush_from_home_timelines(account, statuses)
recipients = account.followers.local.to_a
@@ -123,23 +101,9 @@ class BatchedRemoveStatusService < BaseService
Redis.current
end
def build_json(status)
return @activity_json[status.id] if @activity_json.key?(status.id)
@activity_json[status.id] = sign_json(status, ActiveModelSerializers::SerializableResource.new(
status,
serializer: status.reblog? ? ActivityPub::UndoAnnounceSerializer : ActivityPub::DeleteSerializer,
adapter: ActivityPub::Adapter
).as_json)
end
def build_xml(stream_entry)
return @activity_xml[stream_entry.id] if @activity_xml.key?(stream_entry.id)
@activity_xml[stream_entry.id] = stream_entry_to_xml(stream_entry)
end
def sign_json(status, json)
Oj.dump(ActivityPub::LinkedDataSignature.new(json).sign!(status.account))
end
end

View File

@@ -50,7 +50,7 @@ class FetchAtomService < BaseService
@unsupported_activity = true
nil
end
elsif @response['Link'] && !terminal
elsif @response['Link'] && !terminal && link_header.find_link(%w(rel alternate))
process_headers
elsif @response.mime_type == 'text/html' && !terminal
process_html
@@ -70,8 +70,6 @@ class FetchAtomService < BaseService
end
def process_headers
link_header = LinkHeader.parse(@response['Link'].is_a?(Array) ? @response['Link'].first : @response['Link'])
json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'])
atom_link = link_header.find_link(%w(rel alternate), %w(type application/atom+xml))
@@ -80,4 +78,8 @@ class FetchAtomService < BaseService
result
end
def link_header
@link_header ||= LinkHeader.parse(@response['Link'].is_a?(Array) ? @response['Link'].first : @response['Link'])
end
end

View File

@@ -22,6 +22,8 @@ class SuspendAccountService < BaseService
end
def purge_content!
ActivityPub::RawDistributionWorker.perform_async(delete_actor_json, @account.id) if @account.local?
@account.statuses.reorder(nil).find_in_batches do |statuses|
BatchedRemoveStatusService.new.call(statuses)
end
@@ -54,4 +56,14 @@ class SuspendAccountService < BaseService
def destroy_all(association)
association.in_batches.destroy_all
end
def delete_actor_json
payload = ActiveModelSerializers::SerializableResource.new(
@account,
serializer: ActivityPub::DeleteActorSerializer,
adapter: ActivityPub::Adapter
).as_json
Oj.dump(ActivityPub::LinkedDataSignature.new(payload).sign!(@account))
end
end

View File

@@ -160,6 +160,7 @@ pl:
update_status: "%{name} zaktualizował wpis użytkownika %{target}"
title: Dziennik działań administracyjnych
custom_emojis:
by_domain: Według domeny
copied_msg: Pomyślnie utworzono lokalną kopię emoji
copy: Kopiuj
copy_failed_msg: Nie udało się utworzyć lokalnej kopii emoji
@@ -612,6 +613,7 @@ pl:
private: Nie możesz przypiąć niepublicznego wpisu
reblog: Nie możesz przypiąć podbicia wpisu
show_more: Pokaż więcej
title: '%{name}: "%{quote}"'
visibilities:
private: Tylko dla śledzących
private_long: Widoczne tylko dla osób, które Cię śledzą

View File

@@ -21,7 +21,7 @@ zh-TW:
data: 資料
display_name: 顯示名稱
email: 電子信箱
filtered_languages: 封鎖下面言的文章
filtered_languages: 封鎖下面言的文章
header: 個人頁面頂部
locale: 語言
locked: 將帳號轉為「私密」
@@ -29,7 +29,16 @@ zh-TW:
note: 簡介
otp_attempt: 雙因子驗證碼
password: 密碼
setting_auto_play_gif: 自動播放 GIFs
setting_boost_modal: 轉推前跳出確認視窗
setting_default_privacy: 文章預設隱私度
setting_default_sensitive: 預設我的內容為敏感內容
setting_delete_modal: 刪推前跳出確認視窗
setting_noindex: 不被搜尋引擎檢索
setting_reduce_motion: 減低動畫效果
setting_system_font_ui: 使用系統預設字體
setting_theme: 網站主題
setting_unfollow_modal: 取消關注前跳出確認視窗
type: 匯入資料類型
username: 使用者名稱
interactions:

View File

@@ -55,7 +55,7 @@ zh-TW:
perform_full_suspension: 進行停權
profile_url: 個人檔案網址
public: 公開
push_subscription_expires: PuSH 訂閱
push_subscription_expires: 推播訂閱
salmon_url: Salmon URL
silence: 靜音
statuses: 狀態
@@ -133,12 +133,14 @@ zh-TW:
forgot_password: 忘記密碼?
login: 登入
logout: 登出
migrate_account: 轉移到另一個帳號
migrate_account_html: 想要將這個帳號指向另一個帳號可到<a href="%{path}">到這裡設定</a>。
register: 註冊
resend_confirmation: 重寄驗證信
reset_password: 重設密碼
set_new_password: 設定新密碼
authorize_follow:
error: 對不起,尋找這個跨站使用者的過程發生錯誤
error: 對不起,搜尋遠端使用者出現錯誤
follow: 關注
title: 關注 %{acct}
datetime:
@@ -165,7 +167,16 @@ zh-TW:
blocks: 您封鎖的使用者
csv: CSV
follows: 您關注的使用者
mutes: 您靜音的使用者
storage: 儲存空間大小
followers:
domain: 網域
explanation_html: 為確保個人隱私,您必須知道有哪些使用者正關注你。<strong>您的私密內容會被發送到所有您有被關注的服務站上</strong>。如果您不信任這些服務站的管理者,您可以選擇檢查或刪除您的關注者。
followers_count: 關注者數
lock_link: 鎖住你的帳號
purge: 移除關注者
unlocked_warning_html: 所有人都可以關注並檢索你的隱藏狀態。%{lock_link}以檢查或拒絕關注。
unlocked_warning_title: 你的帳號是公開的
generic:
changes_saved_msg: 已成功儲存修改
powered_by: 網站由 %{link} 開發
@@ -179,6 +190,7 @@ zh-TW:
types:
blocking: 您封鎖的使用者名單
following: 您關注的使用者名單
muting: 您靜音的使用者名單
upload: 上傳
landing_strip_html: "<strong>%{name}</strong> 是一個在 %{link_to_root_path} 的使用者。只要您有任何 Mastodon 服務站、或者聯盟網站的帳號,便可以跨站關注此站使用者,或者與他們互動。"
landing_strip_signup_html: 如果您沒有這些帳號,歡迎在<a href="%{sign_up_path}">這裡註冊</a>。
@@ -231,15 +243,26 @@ zh-TW:
missing_resource: 無法找到資源
proceed: 下一步
prompt: 您希望關注︰
sessions:
activity: 最近活動
browser: 瀏覽器
current_session: 目前的 session
description: "%{platform} 上的 %{browser}"
explanation: 這些是現在正登入於你的 Mastodon 帳號的瀏覽器。
revoke: 取消
revoke_success: Session 取消成功。
settings:
authorized_apps: 已授權應用程式
back: 回到 Mastodon
development: 開發
edit_profile: 修改個人資料
export: 匯出
followers: 授權追蹤者
import: 匯入
notifications: 通知
preferences: 偏好設定
settings: 設定
two_factor_authentication: 雙因子認證
two_factor_authentication: 兩階段認證
statuses:
open_in_web: 以網頁開啟
over_character_limit: 超過了 %{max} 字的限制
@@ -257,14 +280,14 @@ zh-TW:
default: "%Y年%-m月%d日 %H:%M"
two_factor_authentication:
code_hint: 請輸入您認證器產生的代碼,以進行認證
description_html: 當您啟用<strong>雙因子認證</strong>後,登入時將需要使手機、或其他種類認證器產生的代碼。
description_html: 啟用<strong>兩階段認證</strong>後,登入時將需要使手機、或其他種類認證器產生的代碼。
disable: 停用
enable: 啟用
enabled_success: 已成功啟用雙因子認證
instructions_html: "<strong>請用您手機的認證器應用程式(如 Google Authenticator、Authy掃描這裡的 QR 圖形碼</strong>。在雙因子認證啟用後,您登入時將須要使用此應用程式產生的認證碼。"
enabled_success: 已成功啟用兩階段認證
instructions_html: "<strong>請用您手機的認證器應用程式(如 Google Authenticator、Authy掃描這裡的 QR 圖形碼</strong>。在兩階段認證啟用後,您登入時將須要使用此應用程式產生的認證碼。"
manual_instructions: 如果您無法掃描 QR 圖形碼,請手動輸入︰
setup: 設定
wrong_code: 您輸入的認證碼並不正確!可能伺服器時間和您手機不一致,請檢查您手機的時間,或與本站管理員聯絡。
users:
invalid_email: 信箱地址格式不正確
invalid_otp_token: 雙因子認證碼不正確
invalid_otp_token: 兩階段認證碼不正確