From 7831f0495111ad2e38c8b5ef54ba9fd0872aa3a2 Mon Sep 17 00:00:00 2001 From: John Hensley Date: Mon, 4 Jan 2021 14:56:15 -0500 Subject: [PATCH 1/3] Add simplified Chinese to the list of supported languages --- securedrop/i18n_tool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/securedrop/i18n_tool.py b/securedrop/i18n_tool.py index 10b7cb1571..a28b6ffe28 100755 --- a/securedrop/i18n_tool.py +++ b/securedrop/i18n_tool.py @@ -56,6 +56,7 @@ class I18NTool: 'sk': {'name': 'Slovak', 'desktop': 'sk', }, 'sv': {'name': 'Swedish', 'desktop': 'sv', }, 'tr': {'name': 'Turkish', 'desktop': 'tr', }, + 'zh_Hans': {'name': 'Chinese, Simplified', 'desktop': 'zh_Hans', }, 'zh_Hant': {'name': 'Chinese, Traditional', 'desktop': 'zh_Hant', }, } release_tag_re = re.compile(r"^\d+\.\d+\.\d+$") From 06d053eef0dc70f635ec626bb51e88fe3cb8eb18 Mon Sep 17 00:00:00 2001 From: John Hensley Date: Mon, 4 Jan 2021 15:23:14 -0500 Subject: [PATCH 2/3] l10n: updated Chinese, Simplified (zh_Hans) contributors: updated from: repo: https://github.com/freedomofpress/securedrop-i18n commit: e921bb11f2d835b20df7d456ec19e9d06943366e --- .../roles/tails-config/templates/zh_Hans.po | 27 + .../zh_Hans/LC_MESSAGES/messages.po | 1470 +++++++++++++++++ 2 files changed, 1497 insertions(+) create mode 100644 install_files/ansible-base/roles/tails-config/templates/zh_Hans.po create mode 100644 securedrop/translations/zh_Hans/LC_MESSAGES/messages.po diff --git a/install_files/ansible-base/roles/tails-config/templates/zh_Hans.po b/install_files/ansible-base/roles/tails-config/templates/zh_Hans.po new file mode 100644 index 0000000000..ea1f4694fb --- /dev/null +++ b/install_files/ansible-base/roles/tails-config/templates/zh_Hans.po @@ -0,0 +1,27 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR Freedom of the Press Foundation +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: securedrop@freedom.press\n" +"PO-Revision-Date: 2020-05-05 09:06+0000\n" +"Last-Translator: ff98sha \n" +"Language-Team: Chinese (Simplified) \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 3.10.3\n" + +#: desktop-journalist-icon.j2.in:10 +msgid "SecureDrop Journalist Interface" +msgstr "SecureDrop 记者界面" + +#: desktop-source-icon.j2.in:10 +msgid "SecureDrop Source Interface" +msgstr "SecureDrop 线人界面" diff --git a/securedrop/translations/zh_Hans/LC_MESSAGES/messages.po b/securedrop/translations/zh_Hans/LC_MESSAGES/messages.po new file mode 100644 index 0000000000..71d1b72751 --- /dev/null +++ b/securedrop/translations/zh_Hans/LC_MESSAGES/messages.po @@ -0,0 +1,1470 @@ +# Translations template for SecureDrop. +# Copyright (C) 2018 Freedom of the Press Foundation +# This file is distributed under the same license as the SecureDrop project. +# FIRST AUTHOR , 2018. +# +msgid "" +msgstr "" +"Project-Id-Version: SecureDrop 0.6~rc2\n" +"Report-Msgid-Bugs-To: securedrop@freedom.press\n" +"PO-Revision-Date: 2020-10-02 15:19+0000\n" +"Last-Translator: erinm \n" +"Language-Team: Chinese (Simplified) \n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 4.1.1\n" +"Generated-By: Babel 2.5.1\n" + +#: template_filters.py:14 +msgid "{time} ago" +msgstr "{time} 之前" + +#: journalist_app/__init__.py:90 journalist_app/__init__.py:131 +msgid "You have been logged out due to inactivity." +msgstr "您因过久未活动而被登出。" + +#: journalist_app/__init__.py:140 +msgid "You have been logged out due to password change" +msgstr "您因密码更改而被登出" + +#: journalist_app/account.py:36 +msgid "Incorrect password or two-factor code." +msgstr "密码或双重验证的验证码不正确。" + +#: journalist_app/account.py:52 +msgid "Your two-factor credentials have been reset successfully." +msgstr "已成功重置您的两步认证凭证。" + +#: journalist_app/account.py:56 journalist_app/admin.py:136 +msgid "There was a problem verifying the two-factor code. Please try again." +msgstr "验证两步认证码时出现问题。请稍后重试。" + +#: journalist_app/admin.py:42 +msgid "Image updated." +msgstr "图片已更新。" + +#: journalist_app/admin.py:62 +msgid "Preferences saved." +msgstr "设置已保存。" + +#: journalist_app/admin.py:92 +msgid "" +"There was an error with the autogenerated password. User not created. Please " +"try again." +msgstr "自动生成密码时发生错误。用户账户未建立。请重试。" + +#: journalist_app/admin.py:103 journalist_app/admin.py:186 +msgid "Username \"{user}\" already taken." +msgstr "用户名 \"{user}\" 已经被占用。" + +#: journalist_app/admin.py:106 +msgid "" +"An error occurred saving this user to the database. Please inform your admin." +msgstr "保存该用户到数据库时出错。请通知管理员。" + +#: journalist_app/admin.py:130 +msgid "The two-factor code for user \"{user}\" was verified successfully." +msgstr "已成功验证用户 \"{user}\" 的两步验证代码。" + +#: journalist_app/admin.py:200 journalist_app/admin.py:208 +#: journalist_app/utils.py:350 +msgid "Name not updated: {}" +msgstr "未更新名称: {}" + +#: journalist_app/admin.py:232 +msgid "Deleted user '{user}'." +msgstr "已删除用户'{user}'。" + +#: journalist_app/admin.py:262 +msgid "Test alert sent. Please check your email." +msgstr "已发送测试警报。请检查您的邮箱。" + +#: journalist_app/col.py:60 +msgid "{source_name}'s collection deleted." +msgstr "{source_name} 集合已删除。" + +#: journalist_app/col.py:71 +msgid "No collections selected." +msgstr "未选择任何的集合。" + +#: journalist_app/decorators.py:15 +msgid "Only admins can access this page." +msgstr "此页面仅供管理员访问。" + +#: journalist_app/forms.py:19 +msgid "HOTP secrets are 40 characters long - you have entered {num_chars}." +msgstr "HOTP 密码长度为40个字符 - 您已输入 {num_chars} 个字符。" + +#: journalist_app/forms.py:28 +msgid "Must be at least {min_chars} characters long." +msgstr "该字段需要至少 {min_chars} 个字符。" + +#: journalist_app/forms.py:35 +msgid "Cannot be longer than {max_chars} characters." +msgstr "字段长度需小于 {max_chars} 个字符。" + +#: journalist_app/forms.py:40 +msgid "" +"This username is invalid because it is reserved for internal use by the " +"software." +msgstr "此用户名无效,因为其被保留为软件内部使用。" + +#: journalist_app/forms.py:46 source_app/forms.py:12 +msgid "This field is required." +msgstr "这是必填项。" + +#: journalist_app/forms.py:65 +msgid "You cannot send an empty reply." +msgstr "回复内容不可空白。" + +#: journalist_app/forms.py:77 +msgid "File required." +msgstr "该文件是必选项。" + +#: journalist_app/forms.py:79 +msgid "You can only upload PNG image files." +msgstr "您只能上传 PNG 图片文件。" + +#: journalist_app/main.py:124 journalist_app/utils.py:58 +#: journalist_app/utils.py:157 +msgid "An unexpected error occurred! Please inform your admin." +msgstr "出现一个意外的错误!请通知系统管理员。" + +#: journalist_app/main.py:135 +msgid "Thanks. Your reply has been stored." +msgstr "谢谢,您的回复已经被储存。" + +#: journalist_app/main.py:156 +msgid "No collections selected for download." +msgstr "未选择要下载的集合。" + +#: journalist_app/main.py:159 journalist_app/utils.py:284 +msgid "No collections selected for deletion." +msgstr "未选择要删除的集合。" + +#: journalist_app/main.py:180 +msgid "No unread submissions for this source." +msgstr "该线人无未读内容。" + +#: journalist_app/utils.py:65 +msgid "Account updated." +msgstr "账号已更新。" + +#: journalist_app/utils.py:106 +msgid "Login failed." +msgstr "登录失败。" + +#: journalist_app/utils.py:113 +msgid "Please wait at least {seconds} second before logging in again." +msgid_plural "Please wait at least {seconds} seconds before logging in again." +msgstr[0] "请至少等待 {seconds} 秒后再登录。" + +#: journalist_app/utils.py:124 +msgid "" +"Please wait for a new code from your two-factor mobile app or security key " +"before trying again." +msgstr "请在重试前稍等您的两步验证手机应用程序或安全密钥刷新。" + +#: journalist_app/utils.py:145 +msgid "Invalid secret format: please only submit letters A-F and numbers 0-9." +msgstr "无效密码格式:请使用 A-F 和数字 0-9。" + +#: journalist_app/utils.py:151 +msgid "Invalid secret format: odd-length secret. Did you mistype the secret?" +msgstr "无效密码格式:奇数长度的密钥。您是不是输错了?" + +#: journalist_app/utils.py:236 +msgid "Submission deleted." +msgid_plural "{num} submissions deleted." +msgstr[0] "{num} 个内容已删除。" + +#: journalist_app/utils.py:292 +msgid "{num} collection deleted" +msgid_plural "{num} collections deleted" +msgstr[0] "{num} 个集合已删除" + +#: journalist_app/utils.py:348 +msgid "Name updated." +msgstr "已更新名称。" + +#: journalist_app/utils.py:357 +msgid "The password you submitted is invalid. Password not changed." +msgstr "您提交的密码无效!密码未更改。" + +#: journalist_app/utils.py:364 +msgid "" +"There was an error, and the new password might not have been saved " +"correctly. To prevent you from getting locked out of your account, you " +"should reset your password again." +msgstr "发生错误,新密码可能未存储成功。请再次设置密码以免账号被锁定。" + +#: journalist_app/utils.py:373 +msgid "" +"Password updated. Don't forget to save it in your KeePassX database. New " +"password:" +msgstr "密码已更新。别忘了存到 KeePassX 数据库里哦。新密码:" + +#: journalist_app/utils.py:397 +msgid "No unread submissions in selected collections." +msgstr "选定的集合中没有未读内容。" + +#: journalist_templates/_confirmation_modal.html:4 +msgid "Close" +msgstr "关闭" + +#: journalist_templates/_confirmation_modal.html:10 +#: source_templates/lookup.html:104 +msgid "Cancel" +msgstr "取消" + +#: journalist_templates/_source_row.html:23 +msgid "1 doc" +msgid_plural "{doc_num} docs" +msgstr[0] "{doc_num} 文档" + +#: journalist_templates/_source_row.html:24 +msgid "1 message" +msgid_plural "{msg_num} messages" +msgstr[0] "{msg_num} 消息" + +#: journalist_templates/_source_row.html:27 +msgid "1 unread" +msgid_plural "{num_unread} unread" +msgstr[0] "{num_unread} 未读" + +#: journalist_templates/account_edit_hotp_secret.html:6 +#: journalist_templates/admin_edit_hotp_secret.html:7 +msgid "Change Secret" +msgstr "更改密码" + +#: journalist_templates/account_edit_hotp_secret.html:7 +#: journalist_templates/admin_add_user.html:68 +#: journalist_templates/admin_edit_hotp_secret.html:8 +msgid "HOTP Secret" +msgstr "HOTP 密码" + +#: journalist_templates/account_edit_hotp_secret.html:9 +#: journalist_templates/admin_edit_hotp_secret.html:10 +#: source_templates/login.html:21 +msgid "CONTINUE" +msgstr "继续" + +#: journalist_templates/account_new_two_factor.html:4 +#: journalist_templates/admin_new_user_two_factor.html:5 +msgid "Enable FreeOTP" +msgstr "启用 FreeOTP" + +#: journalist_templates/account_new_two_factor.html:5 +msgid "" +"You're almost done! To finish resetting your two-factor authentication, " +"follow the instructions below to set up FreeOTP. Once you've added the entry " +"for your account in the app, enter one of the 6-digit codes from the app to " +"confirm that two-factor authentication is set up correctly." +msgstr "" +"就快完成了!要完成您的两步验证,请遵循下方指示设置 FreeOTP。在应用程序内添加" +"完账户后,请输入显示的 6 位代码以验证您已正确完成两步验证配置。" + +#: journalist_templates/account_new_two_factor.html:8 +#: journalist_templates/admin_new_user_two_factor.html:9 +msgid "Install FreeOTP on your phone" +msgstr "在您的手机上安装 FreeOTP" + +#: journalist_templates/account_new_two_factor.html:9 +#: journalist_templates/admin_new_user_two_factor.html:10 +msgid "Open the FreeOTP app" +msgstr "打开 FreeOTP 应用" + +#: journalist_templates/account_new_two_factor.html:10 +#: journalist_templates/admin_new_user_two_factor.html:11 +msgid "Tap the QR code symbol at the top" +msgstr "点按顶部的二维码图标" + +#: journalist_templates/account_new_two_factor.html:11 +#: journalist_templates/admin_new_user_two_factor.html:12 +msgid "" +"Your phone will now be in \"scanning\" mode. When you are in this mode, scan " +"the barcode below:" +msgstr "您的手机将处于 “扫描” 模式。这时,扫下面的条形码:" + +#: journalist_templates/account_new_two_factor.html:14 +msgid "" +"Can't scan the barcode? You can manually pair FreeOTP with your SecureDrop " +"account by entering the following two-factor secret into the app:" +msgstr "" +"无法扫描二维码?您可手动输入下列两步验证密钥以将您的 SecureDrop 账户关联至 " +"FreeOTP:" + +#: journalist_templates/account_new_two_factor.html:16 +#: journalist_templates/admin_new_user_two_factor.html:18 +msgid "" +"Once you have paired FreeOTP with this account, enter the 6-digit " +"verification code below:" +msgstr "当你配对此账户至 FreeOTP 后,请输入下方的 6 位验证码:" + +#: journalist_templates/account_new_two_factor.html:18 +#: journalist_templates/admin_new_user_two_factor.html:20 +msgid "Enable YubiKey (OATH-HOTP)" +msgstr "启用 YubiKey(OATH-HOTP)" + +#: journalist_templates/account_new_two_factor.html:19 +msgid "" +"Once you have configured your YubiKey, enter the 6-digit verification code " +"below:" +msgstr "当您配置好 YubiKey 后,请输入下方的 6 位验证码:" + +#: journalist_templates/account_new_two_factor.html:23 +#: journalist_templates/admin_new_user_two_factor.html:25 +msgid "Verification code" +msgstr "验证码" + +#: journalist_templates/account_new_two_factor.html:25 +#: journalist_templates/admin_new_user_two_factor.html:27 +#: journalist_templates/col.html:89 source_templates/lookup.html:69 +msgid "SUBMIT" +msgstr "提交" + +#: journalist_templates/admin.html:3 +msgid "Admin Interface" +msgstr "管理员界面" + +#: journalist_templates/admin.html:6 +#: journalist_templates/admin_add_user.html:74 +msgid "ADD USER" +msgstr "添加用户" + +#: journalist_templates/admin.html:16 +#: journalist_templates/admin_add_user.html:14 +#: journalist_templates/edit_account.html:12 journalist_templates/login.html:8 +msgid "Username" +msgstr "用户名" + +#: journalist_templates/admin.html:17 +msgid "Edit" +msgstr "编辑" + +#: journalist_templates/admin.html:18 journalist_templates/col.html:125 +#: journalist_templates/index.html:16 journalist_templates/index.html:52 +msgid "Delete" +msgstr "删除" + +#: journalist_templates/admin.html:19 +msgid "Created" +msgstr "已创建" + +#: journalist_templates/admin.html:20 +msgid "Last login" +msgstr "最近登录" + +#: journalist_templates/admin.html:25 +msgid "Edit user {username}" +msgstr "编辑用户 {username}" + +#: journalist_templates/admin.html:29 +msgid "Delete user {username}" +msgstr "删除用户 {username}" + +#: journalist_templates/admin.html:35 +msgid "never" +msgstr "从未" + +#: journalist_templates/admin.html:42 +msgid "No users to display" +msgstr "无用户可显示" + +#: journalist_templates/admin.html:48 +msgid "INSTANCE CONFIG" +msgstr "实例配置" + +#: journalist_templates/admin_add_user.html:4 +#: journalist_templates/config.html:4 journalist_templates/edit_account.html:7 +msgid "Back to admin interface" +msgstr "回到管理员页面" + +#: journalist_templates/admin_add_user.html:24 +msgid "Username can contain spaces" +msgstr "用户名可以包含空格" + +#: journalist_templates/admin_add_user.html:25 +msgid "Username is case-sensitive" +msgstr "用户名需区分大小写" + +#: journalist_templates/admin_add_user.html:32 +#: journalist_templates/edit_account.html:16 +#: journalist_templates/edit_account.html:36 +msgid "First name" +msgstr "名" + +#: journalist_templates/admin_add_user.html:42 +#: journalist_templates/edit_account.html:19 +#: journalist_templates/edit_account.html:39 +msgid "Last name" +msgstr "姓" + +#: journalist_templates/admin_add_user.html:52 +msgid "First name and last name are optional" +msgstr "姓与名均为选填项" + +#: journalist_templates/admin_add_user.html:56 +msgid "The user's password will be:" +msgstr "此用户的密码为:" + +#: journalist_templates/admin_add_user.html:59 +#: journalist_templates/edit_account.html:25 +msgid "Is Admin" +msgstr "是否为管理员" + +#: journalist_templates/admin_add_user.html:67 +msgid "Is using a YubiKey [HOTP]" +msgstr "是否使用 YubiKey [HOTP]" + +#: journalist_templates/admin_new_user_two_factor.html:6 +msgid "" +"You're almost done! To finish adding this new user, have them follow the " +"instructions below to set up two-factor authentication with FreeOTP. Once " +"they've added an entry for this account in the app, have them enter one of " +"the 6-digit codes from the app to confirm that two-factor authentication is " +"set up correctly." +msgstr "" +"就快完成了!要完成新用户的添加,请让他们按照下面的说明去设置 FreeOTP 双重验" +"证。在他们添加账号到 app 里后,让他们输入应用中其中一个 6 位密码,以确认双重" +"验证正确设置了。" + +#: journalist_templates/admin_new_user_two_factor.html:15 +msgid "" +"Can't scan the barcode? You can manually pair FreeOTP with this account by " +"entering the following two-factor secret into the app:" +msgstr "" +"无法扫描二维码?您可手动输入下列两步验证密钥以将您的账户关联至 FreeOTP:" + +#: journalist_templates/admin_new_user_two_factor.html:21 +msgid "Once you have configured your YubiKey, enter the 6-digit code below:" +msgstr "当你配置好 YubiKey 后,输入下面的 6 位口令:" + +#: journalist_templates/base.html:24 +msgid "" +"Update Required: Your SecureDrop servers are still running " +"v2 onion services, which are being phased out for security reasons. In " +"February 2021, v2 onion services will be disabled, and your SecureDrop " +"servers may become unreachable. Learn More" +msgstr "" +"需要更新:你的 SecureDrop 服务器仍在使用v2版洋葱服务,其因为" +"安全问题已过时。在2021年2月,v2版洋葱服务将被禁用,届时你的 SecureDrop 服务器" +"会无法访问。了解" +"更多" + +#: journalist_templates/base.html:29 +msgid "Logged on as" +msgstr "以身份登录" + +#: journalist_templates/base.html:31 +msgid "Admin" +msgstr "管理员" + +#: journalist_templates/base.html:33 +msgid "Log Out" +msgstr "登出" + +#: journalist_templates/base.html:56 +msgid "Powered by SecureDrop {version}." +msgstr "由 SecureDrop 提供支持。" + +#: journalist_templates/col.html:6 +msgid "All Sources" +msgstr "所有线人" + +#: journalist_templates/col.html:12 +msgid "" +"The documents are stored encrypted for security. To read them, you will need " +"to decrypt them using GPG." +msgstr "安全起见,所有文件都经过加密存储。如果想要阅读,必须使用GPG解密。" + +#: journalist_templates/col.html:16 +msgid "Download Selected" +msgstr "下载所选的部分" + +#: journalist_templates/col.html:18 +msgid "Delete Selected" +msgstr "删除所选的部分" + +#: journalist_templates/col.html:28 journalist_templates/col.html:48 +#: journalist_templates/col.html:81 +msgid "Reply" +msgstr "回复" + +#: journalist_templates/col.html:31 journalist_templates/col.html:38 +msgid "Read" +msgstr "读取" + +#: journalist_templates/col.html:35 +msgid "Unread" +msgstr "未读" + +#: journalist_templates/col.html:46 +msgid "Uploaded Document" +msgstr "已上传的文件" + +#: journalist_templates/col.html:50 +msgid "Message" +msgstr "消息" + +#: journalist_templates/col.html:60 journalist_templates/col.html:120 +#: journalist_templates/index.html:46 +msgid "Delete Confirmation" +msgstr "确认删除" + +#: journalist_templates/col.html:61 +msgid "Are you sure you want to delete the selected documents?" +msgstr "你确定要删除这些选中的文件吗?" + +#: journalist_templates/col.html:65 source_templates/lookup.html:105 +msgid "DELETE" +msgstr "删除" + +#: journalist_templates/col.html:76 +msgid "No documents to display." +msgstr "无文件可显示。" + +#: journalist_templates/col.html:83 +msgid "" +"You can write a secure reply to the person who submitted these documents:" +msgstr "您可以撰写一个私密的回复给文件的提供者:" + +#: journalist_templates/col.html:92 +msgid "You've flagged this source for reply." +msgstr "您已经把这名线人标记为待回复。" + +#: journalist_templates/col.html:93 +msgid "" +"An encryption key will be generated for the source the next time they log " +"in, after which you will be able to reply to the source here." +msgstr "系统会在下次线人登陆时生成新的加密密钥,之后你可以在这里回复线人。" + +#: journalist_templates/col.html:95 +msgid "Click below if you would like to write a reply to this source." +msgstr "如果要回复此名线人,请点击下方。" + +#: journalist_templates/col.html:99 +msgid "FLAG THIS SOURCE FOR REPLY" +msgstr "标记此线人为待回复" + +#: journalist_templates/col.html:104 +msgid "" +"Click below to delete this source's collection. Warning: If you do this, " +"the files seen here will be unrecoverable and the source will no longer be " +"able to login using their previous codename." +msgstr "" +"点击下方来删除这名线人的文件。警告:如果你这么做,这里看到的文件无法恢" +"复,且线人将无法用之前的代号登陆。" + +#: journalist_templates/col.html:112 +msgid "DELETE SOURCE AND SUBMISSIONS" +msgstr "删除线人与其提交的资料" + +#: journalist_templates/col.html:121 +msgid "Are you sure you want to delete this collection?" +msgstr "确认要删除这些文件?" + +#: journalist_templates/config.html:7 +msgid "Instance Configuration" +msgstr "实例设置" + +#: journalist_templates/config.html:9 +msgid "Alerts" +msgstr "警告" + +#: journalist_templates/config.html:11 +msgid "Send an encrypted email to verify if OSSEC alerts work correctly:" +msgstr "发送一封加密的邮件已验证OSSEC是否正确工作:" + +#: journalist_templates/config.html:15 +msgid "SEND TEST OSSEC ALERT" +msgstr "发送测试OSSEC警报" + +#: journalist_templates/config.html:21 source_templates/base.html:20 +#: source_templates/index.html:29 +msgid "Logo Image" +msgstr "图标" + +#: journalist_templates/config.html:23 +msgid "" +"Here you can update the image displayed on the SecureDrop web interfaces:" +msgstr "在此处可更新SecureDrop网页界面的显示图片:" + +#: journalist_templates/config.html:36 +msgid "Recommended size: 500px * 450px" +msgstr "建议最佳尺寸:500px * 450px" + +#: journalist_templates/config.html:39 +msgid "UPDATE LOGO" +msgstr "更新图标" + +#: journalist_templates/config.html:46 +msgid "Submission Preferences" +msgstr "提交设置" + +#: journalist_templates/config.html:52 +msgid "" +"Prevent sources from uploading documents. Sources will still be able to send " +"messages." +msgstr "阻止线人上传文件,其将仍可发送信息。" + +#: journalist_templates/config.html:55 +msgid "UPDATE SUBMISSION PREFERENCES" +msgstr "更新提交设置" + +#: journalist_templates/delete.html:5 +msgid "" +"The following file has been selected for permanent deletion:" +msgid_plural "" +"The following {files} files have been selected for permanent " +"deletion:" +msgstr[0] "下列选中的 {files} 文件将被永久删除:" + +#: journalist_templates/delete.html:20 +msgid "PERMANENTLY DELETE FILES" +msgstr "永久删除文件" + +#: journalist_templates/delete.html:23 +msgid "Return to the list of documents for {source_name}…" +msgstr "返回 {source_name} 的文件列表…" + +#: journalist_templates/edit_account.html:6 +msgid "Edit user \"{user}\"" +msgstr "编辑账户 \"{user}\"" + +#: journalist_templates/edit_account.html:8 +msgid "Change Name and Admin Status" +msgstr "更改名称和管理员状态" + +#: journalist_templates/edit_account.html:27 +#: journalist_templates/edit_account.html:43 +msgid "UPDATE" +msgstr "更新" + +#: journalist_templates/edit_account.html:30 +msgid "Edit your account" +msgstr "编辑你的账户" + +#: journalist_templates/edit_account.html:31 +msgid "Change Name" +msgstr "更改名字" + +#: journalist_templates/edit_account.html:47 +#: journalist_templates/edit_account.html:75 +msgid "Reset Password" +msgstr "重置密码" + +#: journalist_templates/edit_account.html:49 +msgid "SecureDrop uses automatically generated diceware passwords." +msgstr "SecureDrop 使用自动生成的 Diceware 密码。" + +#: journalist_templates/edit_account.html:50 +msgid "" +"Your password will be changed immediately, so you will need to save it " +"before pressing the \"Reset Password\" button." +msgstr "你的密码将被立即更改,你需要在按下“重置密码”前保存。" + +#: journalist_templates/edit_account.html:56 +msgid "Please enter your current password and two-factor code." +msgstr "请输入目前使用的密码与双重认证密钥。" + +#: journalist_templates/edit_account.html:61 +msgid "Current Password" +msgstr "当前密码" + +#: journalist_templates/edit_account.html:62 journalist_templates/login.html:11 +msgid "Two-factor Code" +msgstr "双重认证密钥" + +#: journalist_templates/edit_account.html:68 +msgid "The user's password will be changed to:" +msgstr "用户的密码将被修改成:" + +#: journalist_templates/edit_account.html:70 +msgid "Your password will be changed to:" +msgstr "您的密码将被修改成:" + +#: journalist_templates/edit_account.html:80 +msgid "Reset Two-Factor Authentication" +msgstr "重置双重认证授权" + +#: journalist_templates/edit_account.html:83 +msgid "" +"If a user's two-factor authentication credentials have been lost or " +"compromised, you can reset them here. If you do this, make sure the user " +"is present and ready to set up their device with the new two-factor " +"credentials. Otherwise, they will be locked out of their account." +msgstr "" +"如果一个用户的双重认证已丢失或者遭到破坏,可以在此重置它们。如果这么做," +"确认该用户在场,而且在其设置上已设好新的双因素验证。否则,它们的账户将被锁" +"住。" + +#: journalist_templates/edit_account.html:85 +msgid "" +"If your two-factor authentication credentials have been lost or compromised, " +"or you got a new device, you can reset your credentials here. If you do " +"this, make sure you are ready to set up your new device, otherwise you will " +"be locked out of your account." +msgstr "" +"若您的两步认证凭证丢失或泄露,亦或您切换到新设备,您可在此处重置您的凭证。" +"若您重置凭证,请确保您已准备好设置您的新设备,否则您将无法登陆您的账户。" +"" + +#: journalist_templates/edit_account.html:87 +msgid "" +"To reset two-factor authentication for mobile apps such as FreeOTP, choose " +"the first option. For security keys like the YubiKey, choose the second one." +msgstr "" +"要重置移动应用程序的两步认证,如 FreeOTP,请选择第一个选项。要重置安全密钥," +"如 YubiKey,请选择第二个选项。" + +#: journalist_templates/edit_account.html:112 +msgid "Reset two-factor authentication for mobile apps, such as FreeOTP" +msgstr "重置移动应用程序的两步认证,如 FreeOTP" + +#: journalist_templates/edit_account.html:112 +msgid "RESET MOBILE APP CREDENTIALS" +msgstr "重置移动程序凭证" + +#: journalist_templates/edit_account.html:113 +msgid "Reset two-factor authentication for security keys, like a YubiKey" +msgstr "重置安全密钥,如 YubiKey" + +#: journalist_templates/edit_account.html:113 +msgid "RESET SECURITY KEY CREDENTIALS" +msgstr "重置安全密钥凭证" + +#: journalist_templates/flag.html:5 +msgid "Thanks!" +msgstr "万分感谢!" + +#: journalist_templates/flag.html:8 +msgid "" +"SecureDrop will generate a secure encryption key for this source the next " +"time that they log in. Once the key has been generated, a reply box will " +"appear under their collection of documents. You can use this box to write " +"encrypted replies to them." +msgstr "" +"SecureDrop 将在此线人下次登录时为其生成安全加密密钥。生成后,其文档库下将会显" +"示回复框。您可使用此回复框来撰写并向其发送加密信息。" + +#: journalist_templates/flag.html:10 +msgid "Continue to the list of documents for {codename}..." +msgstr "继续至 {codename} 的文档列表···" + +#: journalist_templates/index.html:4 +msgid "Sources" +msgstr "线人" + +#: journalist_templates/index.html:11 +msgid "Download Unread" +msgstr "下载未读项" + +#: journalist_templates/index.html:12 +msgid "Download" +msgstr "下载" + +#: journalist_templates/index.html:13 +msgid "Star" +msgstr "标星" + +#: journalist_templates/index.html:14 +msgid "Un-star" +msgstr "去星" + +#: journalist_templates/index.html:47 +msgid "Are you sure you want to delete the selected collections?" +msgstr "您是否确定要删除选定合集?" + +#: journalist_templates/index.html:48 +msgid "" +"Warning: If you do this, all files for the selected sources will be " +"unrecoverable, and the sources will no longer be able to log in using their " +"previous codename." +msgstr "" +"警告:如果你这么做,选中的线人的所有文件无法恢复,且线人将无法用之前的代号登" +"陆。" + +#: journalist_templates/index.html:60 +msgid "No documents have been submitted!" +msgstr "无已提交的文档!" + +#: journalist_templates/js-strings.html:3 +msgid "filter by codename" +msgstr "按代号筛选" + +#: journalist_templates/js-strings.html:4 +msgid "Select All" +msgstr "选择全部" + +#: journalist_templates/js-strings.html:5 +msgid "Select Unread" +msgstr "选择未读项" + +#: journalist_templates/js-strings.html:6 +msgid "Select None" +msgstr "取消选择" + +#: journalist_templates/js-strings.html:7 +msgid "Are you sure you want to delete the user {username}?" +msgstr "您是否确定要删除用户 {username}?" + +#: journalist_templates/js-strings.html:8 +msgid "" +"Are you sure you want to reset two-factor authentication for {username}?" +msgstr "您是否确定要重置 {username} 的两步认证?" + +#: journalist_templates/login.html:4 +msgid "Login to access the journalist interface" +msgstr "登录以访问记者界面" + +#: journalist_templates/login.html:9 +msgid "Password" +msgstr "密码" + +#: journalist_templates/login.html:10 +msgid "Show password" +msgstr "显示密码" + +#: journalist_templates/login.html:12 source_templates/index.html:59 +#: source_templates/logout.html:3 +msgid "LOG IN" +msgstr "登录" + +#: source_app/__init__.py:117 +msgid "" +"WARNING:  You appear to be using Tor2Web. This " +" does not  provide anonymity. Why is this dangerous?" +msgstr "" +"警告:  您似乎正使用 Tor2Web。此服务 无法" +" 隐藏您的行踪。为何存在风险?" + +#: source_app/forms.py:16 +msgid "Field must be between 1 and {max_codename_len} characters long." +msgstr "字段长度必须介于 1 至 {max_codename_len} 字节之间。" + +#: source_app/forms.py:19 +msgid "Invalid input." +msgstr "输入无效。" + +#: source_app/forms.py:24 +msgid "Write a message." +msgstr "撰写信息。" + +#: source_app/forms.py:29 +msgid "Message text too long." +msgstr "消息文本太长。" + +#: source_app/forms.py:33 +msgid "Large blocks of text must be uploaded as a file, not copied and pasted." +msgstr "大量的文本必须以文件方式上传,而不是复制粘贴。" + +#: source_app/main.py:33 +msgid "" +"You were redirected because you are already logged in. If you want to create " +"a new account, you should log out first." +msgstr "您由于已经登录而被重定向。若您想要创建新账户,请先登出。" + +#: source_app/main.py:64 +msgid "" +"You are already logged in. Please verify your codename above as it may " +"differ from the one displayed on the previous page." +msgstr "您已登录。请验证上方的代号,它可能与上个页面的不同。" + +#: source_app/main.py:80 +msgid "There was a temporary problem creating your account. Please try again." +msgstr "创建账户时出现问题。请稍后重试。" + +#: source_app/main.py:184 +msgid "You must enter a message or choose a file to submit." +msgstr "您必须输入要提交的信息或选择要提交的文件。" + +#: source_app/main.py:188 +msgid "You must enter a message." +msgstr "您必须输入信息。" + +#: source_app/main.py:219 +msgid "Thanks! We received your message." +msgstr "万分感谢!我们收到了您的信息。" + +#: source_app/main.py:221 +msgid "Thanks! We received your document." +msgstr "万分感谢!我们收到了您的文档。" + +#: source_app/main.py:223 +msgid "Thanks! We received your message and document." +msgstr "万分感谢!我们收到了您的信息与文档。" + +#: source_app/main.py:284 +msgid "Reply deleted" +msgstr "回复已删除" + +#: source_app/main.py:302 +msgid "All replies have been deleted" +msgstr "已删除全部回复" + +#: source_app/main.py:316 +msgid "Sorry, that is not a recognized codename." +msgstr "很抱歉,此代号无效。" + +#: source_templates/base.html:6 source_templates/index.html:4 +msgid "Protecting Journalists and Sources" +msgstr "庇护记者及线人" + +#: source_templates/base.html:28 +msgid "LOG OUT" +msgstr "登出" + +#: source_templates/error.html:3 +msgid "Server error" +msgstr "服务器错误" + +#: source_templates/error.html:5 +msgid "" +"Sorry, the website encountered an error and was unable to complete your " +"request." +msgstr "很抱歉,此网站遇到了错误且无法完成您的请求。" + +#: source_templates/error.html:7 source_templates/notfound.html:7 +msgid "Look up a codename..." +msgstr "查询代号···" + +#: source_templates/first_submission_flashed_message.html:2 +msgid "Success!" +msgstr "成功!" + +#: source_templates/first_submission_flashed_message.html:3 +msgid "" +"Thank you for sending this information to us. Please check back later for " +"replies." +msgstr "感谢您向我们提供此信息。请稍后回来查看回复。" + +#: source_templates/first_submission_flashed_message.html:5 +msgid "Forgot your codename?" +msgstr "忘记了您的代号?" + +#: source_templates/footer.html:4 +msgid "Powered by" +msgstr "技术支持" + +#: source_templates/footer.html:7 +msgid "" +"Please note: Sharing sensitive documents may put you at risk, even when " +"using Tor and SecureDrop." +msgstr "" +"请注意:分享敏感文档可能会让您危在旦夕,即便您正使用 Tor 和 SecureDrop。" + +#: source_templates/footer.html:10 +msgid "SecureDrop is a project of Freedom of the Press Foundation." +msgstr "SecureDrop 是媒体自由基金会的项目。" + +#: source_templates/generate.html:4 +msgid "Welcome" +msgstr "欢迎" + +#: source_templates/generate.html:7 +msgid "" +"Please either write this codename down and keep it in a safe place, or " +"memorize it." +msgstr "请写下并妥善保管或牢记此代号。" + +#: source_templates/generate.html:10 +msgid "" +"This codename is what you will use in future visits to receive messages from " +"our team in response to what you submit on the next screen." +msgstr "此代号将是您之后接收我们团队对您在下一界面提交的信息的反馈途径。" + +#: source_templates/generate.html:34 +msgid "" +"Because we do not track users of our SecureDrop\n" +" service, in future visits, using this codename will be the only way we have " +"to communicate with you should we have\n" +" questions or are interested in additional information. Unlike passwords, " +"there is no way to retrieve a lost codename." +msgstr "" +"由于我们不追踪 SecureDrop 的用户,\n" +" 在未来的访问中,使用此代码将是我们与您交流的唯一途径。\n" +" 与密码不同,您无法重设代码。" + +#: source_templates/generate.html:43 +msgid "SUBMIT DOCUMENTS" +msgstr "提交文档" + +#: source_templates/index.html:16 +msgid "" +"Your Tor Browser's Security Level is too low. Use the \"shield " +" button in your browser’s toolbar to change it." +msgstr "" +"您洋葱浏览器的安全等级过" +"低。请点选您浏览器工具栏的 \"盾牌图标\"以更改。" + +#: source_templates/index.html:17 +msgid "" +"It is recommended to use Tor Browser to access SecureDrop: " +"Learn how to install it, or ignore this warning to continue." +msgstr "" +"我们推荐您搭配 Tor 浏览器访问 SecureDrop: 了解如何安装,您也可以忽略此" +"警告继续访问。" + +#: source_templates/index.html:18 +msgid "" +"It is recommended you use the desktop version of Tor Browser to " +"access SecureDrop, as Orfox does not provide the same level of security and " +"anonymity as the desktop version. Learn how to install it, or ignore this warning to " +"continue." +msgstr "" +"我们推荐您使用桌面版 Tor 浏览器访问 SecureDrop,Orfox 相对桌面版而言" +"无法提供相同水准的安全性和匿名性。 了解如何安装,您也可以忽略此警告继续访问。" + +#: source_templates/index.html:39 +msgid "First submission" +msgstr "首次提交" + +#: source_templates/index.html:41 +msgid "First time submitting to our SecureDrop? Start here." +msgstr "首次提交至我们的 SecureDrop?从此开始。" + +#: source_templates/index.html:45 +msgid "GET STARTED" +msgstr "开始入门" + +#: source_templates/index.html:53 +msgid "Return visit" +msgstr "回到访问" + +#: source_templates/index.html:55 +msgid "Already have a codename? Check for replies or submit something new." +msgstr "已有代码?查看回复或提交新内容。" + +#: source_templates/index.html:73 +msgid "Click the \"shield in the toolbar above" +msgstr "点选上方工具栏的 \"盾牌图标\"" + +#: source_templates/index.html:74 +msgid "Select Advanced Security Settings" +msgstr "选择高级安全设置" + +#: source_templates/index.html:75 +msgid "Select Safest" +msgstr "选择最为安全" + +#: source_templates/index.html:77 +msgid "Refresh this page, and you're done!" +msgstr "刷新此页面,大功告成!" + +#: source_templates/login.html:6 +msgid "Enter Codename" +msgstr "输入代码" + +#: source_templates/login.html:12 +msgid "Enter your codename" +msgstr "输入您的代号" + +#: source_templates/login.html:26 source_templates/lookup.html:77 +msgid "CANCEL" +msgstr "取消" + +#: source_templates/logout.html:6 +msgid "One more thing..." +msgstr "还有一件事..." + +#: source_templates/logout.html:7 +msgid "" +"Click the \"broom New Identity button in your Tor Browser's " +"toolbar. This will clear your Tor Browser activity data on this device." +msgstr "" +"点击 Tor 浏览器工具栏的\"broom 新身份按钮。这能清除此设备上的 Tor 浏览" +"器活动记录。" + +#: source_templates/lookup.html:7 +msgid "Remember, your codename is:" +msgstr "请牢记您的代号:" + +#: source_templates/lookup.html:8 +msgid "Show" +msgstr "显示" + +#: source_templates/lookup.html:10 +msgid "Hide" +msgstr "隐藏" + +#: source_templates/lookup.html:26 +msgid "Sorry we haven't responded yet!" +msgstr "抱歉,我们还尚未作答!" + +#: source_templates/lookup.html:27 +msgid "" +"Our SecureDrop recently experienced a surge of activity. For security " +"reasons, the creation of a two-way communication channel was delayed until " +"you checked in again." +msgstr "" +"我们的 SecureDrop 近期正收到大量请求。为了保障安全,我们在您再次登记前延缓了" +"双边交流频道的创建。" + +#: source_templates/lookup.html:28 +msgid "" +"Please rest assured that we were able to download your submission, and check " +"back again later for a reply." +msgstr "请放心我们可以下载您所提交的文件,稍等我们的回复。" + +#: source_templates/lookup.html:35 +msgid "Submit Files or Messages" +msgstr "提交文件或信息" + +#: source_templates/lookup.html:36 +msgid "You can submit any kind of file, a message, or both." +msgstr "您可上传任意种类的文件、信息,或两者均可。" + +#: source_templates/lookup.html:38 +msgid "Submit Messages" +msgstr "提交信息" + +#: source_templates/lookup.html:43 +msgid "" +"If you are already familiar with GPG, you can optionally encrypt your files " +"and messages with our public key " +"before submission. Files are encrypted as they are received by SecureDrop." +msgstr "" +"若您已熟知 GPG,您可在提交前使用我们的" +"公钥加密您的文件及信息。SecureDrop 中的文件将保持加密。" + +#: source_templates/lookup.html:45 +msgid "" +"If you are already familiar with GPG, you can optionally encrypt your " +"messages with our public key " +"before submission." +msgstr "" +"若您已熟知 GPG,您可在提交前使用我们的" +"公钥加密您的信息。" + +#: source_templates/lookup.html:47 +msgid "Learn more." +msgstr "了解更多。" + +#: source_templates/lookup.html:58 +msgid "Maximum upload size: 500 MB" +msgstr "最大文件上传尺寸:500 MB" + +#: source_templates/lookup.html:84 +msgid "Read Replies" +msgstr "阅读回复" + +#: source_templates/lookup.html:89 +msgid "" +"You have received a reply. To protect your identity in the unlikely event " +"someone learns your codename, please delete all replies when you're done " +"with them. This also lets us know that you are aware of our reply. You can " +"respond by submitting new files and messages above." +msgstr "" +"您收到了新回复。为避免您的代码不慎泄露进而暴露您的隐私,请在阅读完回复后删" +"除。同时,我们也足以了解您已知悉我们的回复。您可在上方上传新文件或提交新信息" +"回复。" + +#: source_templates/lookup.html:103 +msgid "Delete this reply?" +msgstr "是否删除此回复?" + +#: source_templates/lookup.html:114 +msgid "DELETE ALL REPLIES" +msgstr "删除全部回复" + +#: source_templates/lookup.html:117 +msgid "Are you finished with the replies?" +msgstr "您是否已阅读完回复?" + +#: source_templates/lookup.html:118 +msgid "YES, DELETE ALL REPLIES" +msgstr "是,删除所有回复" + +#: source_templates/lookup.html:119 +msgid "NO, NOT YET" +msgstr "否,尚未阅读完毕" + +#: source_templates/lookup.html:123 +msgid "— No Messages —" +msgstr "— 无新信息 —" + +#: source_templates/notfound.html:3 +msgid "Page not found" +msgstr "未找到页面" + +#: source_templates/notfound.html:5 +msgid "Sorry, we couldn't locate what you requested." +msgstr "很抱歉,我们无法找到您所请求的内容。" + +#: source_templates/session_timeout.html:2 +msgid "Important" +msgstr "重中之重" + +#: source_templates/session_timeout.html:5 +msgid "" +"You were logged out due to inactivity. Click the \"broom New Identity button " +"in your Tor Browser's toolbar before moving on. This will clear your Tor " +"Browser activity data on this device." +msgstr "" +"您因为长时间无操作而被注销。再继续操作前,点击 Tor 浏览器工具栏上的\"broom 新身份" +"按钮。这能清除此设备上 Tor 浏览器的活动记录。" + +#: source_templates/tor2web-warning.html:3 +msgid "Why is there a warning about Tor2Web?" +msgstr "为何使用 Tor2Web 会有警告信息?" + +#: source_templates/tor2web-warning.html:4 +msgid "Using Tor2Web to connect to SecureDrop will not protect your anonymity." +msgstr "使用 Tor2Web 来连接至 SecureDrop 将无法保护您的匿名身份。" + +#: source_templates/tor2web-warning.html:5 +msgid "" +"It could be possible for anyone monitoring your Internet traffic (your " +"government, your Internet provider), to identify you." +msgstr "监听您网络流量的人(如您的政府、互联网提供商)将可以识别您的身份。" + +#: source_templates/tor2web-warning.html:6 +msgid "" +"We strongly advise you to use the Tor Browser instead." +msgstr "" +"我们强烈建议您使用 Tor 浏览器。" + +#: source_templates/use-tor-browser.html:3 +msgid "You Should Use Tor Browser" +msgstr "您应该使用洋葱浏览器" + +#: source_templates/use-tor-browser.html:4 +msgid "" +"If you are not using Tor Browser, you may not be anonymous." +msgstr "若您使用的不是洋葱浏览器,您可能无法匿名。" + +#: source_templates/use-tor-browser.html:5 +msgid "" +"If you want to submit information to SecureDrop, we strongly advise " +"you to install Tor Browser and use it to access our site safely and " +"anonymously." +msgstr "" +"若您想提交信息至 SecureDrop,我们强烈建议您安装洋葱浏览器并" +"使用其以安全、匿名地访问我们的站点。" + +#: source_templates/use-tor-browser.html:6 +msgid "" +"Copy and paste the following address into your browser and follow the " +"instructions to download and install Tor Browser:" +msgstr "复制粘贴下列地址至您的浏览器,并遵循指示来下载安装洋葱浏览器:" + +#: source_templates/use-tor-browser.html:9 +msgid "" +"If there is a chance that downloading Tor Browser raises suspicion and your " +"mail provider is less likely to be monitored, you can send a mail to " +"
gettor@torproject.org
and a bot will answer with instructions." +msgstr "" +"若您下载洋葱浏览器时起了疑心,且若您的邮件提供商不太可能被监控时,您可发送邮" +"件至
gettor@torproject.org
,机器人将回复您下载指示。" + +#: source_templates/why-journalist-key.html:3 +msgid "Why download the team's public key?" +msgstr "为何要下载团队的公钥?" + +#: source_templates/why-journalist-key.html:4 +msgid "" +"SecureDrop encrypts files and messages after they are submitted. Encrypting " +"messages and files before submission can provide an extra layer of security " +"before your data reaches the SecureDrop server." +msgstr "" +"SecureDrop 将在提交后加密文件及信息。在提交文件至 SecureDrop 服务器前之前进行" +"加密将提供额外的保护。" + +#: source_templates/why-journalist-key.html:5 +msgid "" +"If you are already familiar with the GPG encryption software, you may wish " +"to encrypt your submissions yourself. To do so:" +msgstr "若您已熟知 GPG 加密软件,您可自行加密。请参阅:" + +#: source_templates/why-journalist-key.html:7 +msgid "" +"Download the public key. It will be saved to a file " +"called:\n" +"

{submission_key_fpr_filename}

" +msgstr "" +"下载此公钥。文件名为:\n" +"

{submission_key_fpr_filename}

" + +#: source_templates/why-journalist-key.html:9 +msgid "Import it into your GPG keyring." +msgstr "导入至您的 GPG 密钥串。" + +#: source_templates/why-journalist-key.html:11 +msgid "" +"If you are using Tails, you can double-click the " +".asc file you just downloaded and it will be automatically " +"imported to your keyring." +msgstr "" +"若您正使用 Tails,您可双击您刚下载的 .asc " +"以自动导入至您的密钥串。" + +#: source_templates/why-journalist-key.html:12 +msgid "" +"If you are using macOS or Linux, open the terminal. You can import the key " +"with:

gpg --import /path/to/{submission_key_fpr_filename}

" +msgstr "" +"若您正使用 macOS/Linux,请打开终端。您可使用

gpg --import /path/to/" +"{submission_key_fpr_filename}

导入密钥。" + +#: source_templates/why-journalist-key.html:15 +msgid "Encrypt your submission. Open the terminal and enter this gpg command:" +msgstr "加密你的提交内容。打开命令行并输入此 PGP 命令:" + +#: source_templates/why-journalist-key.html:16 +msgid "" +"gpg --recipient '{submission_key_fpr}' --encrypt /path/to/submission" +msgstr "" +"gpg --recipient '{submission_key_fpr}' --encrypt /path/to/submission" + +#: source_templates/why-journalist-key.html:18 +msgid "" +"Upload your encrypted submission. It will have the same filename as the " +"unencrypted file, with .gpg at the end (e.g. internal_memo.pdf.gpg)" +msgstr "" +"上传您的加密档案,其文件名将与未加密版文件一致,但以 .gpg 文件结尾(如 " +"internal_memo.pdf.gpg)" + +#: source_templates/why-journalist-key.html:21 +msgid "" +"Important: If you wish to remain anonymous, do not use GPG to sign the encrypted file (with the --sign or " +"-s flag) as this will reveal your GPG identity to us." +msgstr "" +"重要提示:若您想保持匿名身份,请勿使用 GPG " +"签名加密文件(即加入 --sign-s 命令行参数),这" +"将向我们泄露您的 GPG 身份。" + +#: source_templates/why-journalist-key.html:23 +msgid "Back to submission page" +msgstr "返回提交页面" + +#~ msgid "" +#~ "Powered by

+#~ " +#~ msgstr "" +#~ "由
\"SecureDrop\" 提" +#~ "供支持" + +#~ msgid "Encrypt your submission." +#~ msgstr "加密您的提交。" + +#~ msgid "" +#~ "You will need to be able to identify the key (this is called the \"user ID" +#~ "\" or UID). Since the public key's filename is the key's fingerprint " +#~ "(with .asc at the end), you can just copy and paste that. (don't include " +#~ "the .asc!)" +#~ msgstr "" +#~ "您将需要识别密钥(名为 “用户 ID” 或简称 UID)。由于公钥文件名即密钥的指纹" +#~ "(以 .asc 结尾),您可直接复制粘贴。(但不包括 .asc 本身!)" + +#~ msgid "" +#~ "On all systems, open the Terminal and use this gpg command: gpg --" +#~ "recipient <user ID> --encrypt roswell_photos.pdf" +#~ msgstr "" +#~ "在任意操作系统上,打开终端并允许此 gpg 命令:gpg --recipient <" +#~ "user ID> --encrypt roswell_photos.pdf" + +#~ msgid "The source '{original_name}' has been renamed to '{new_name}'" +#~ msgstr "线人 '{original_name}' 已被命名为 '{new_name}'" + +#~ msgid "" +#~ "Once you have paired FreeOTP with this account, enter the 6-digit code " +#~ "below:" +#~ msgstr "当你配对此账户至 FreeOTP 后,请输入下方的 6 位验证码:" + +#~ msgid "" +#~ "Generate a new random codename for this source. We recommend doing this " +#~ "if the first random codename is difficult to say or remember. You can " +#~ "generate new random codenames as many times as you like." +#~ msgstr "" +#~ "为这个线人生成一个新的代号。假如第一个随机代号比较复杂或者难记的话,我们建" +#~ "议您这么做。您可以随意生成代号,不限次数。" + +#~ msgid "Change codename" +#~ msgstr "更改代号" + +#~ msgid "Are you sure you want to generate a new codename?" +#~ msgstr "您确定想生成一个新的代号?" + +#~ msgid "CONFIRM" +#~ msgstr "确认" + +#~ msgid "" +#~ "Thank you for exiting your session! Please select \"New Identity\" from " +#~ "the onion button in the Tor browser's toolbar to clear all history of " +#~ "your SecureDrop usage from this device." +#~ msgstr "" +#~ "感谢您退出会话!请点选洋葱浏览器工具栏里的 “新身份” 按钮来清除您在此设备上" +#~ "访问 SecureDrop 的所有记录。" + +#~ msgid "There are no replies at this time." +#~ msgstr "当前没有回复。" + +#~ msgid "" +#~ "Your session timed out due to inactivity. Please login again if you want " +#~ "to continue using SecureDrop, or select \"New Identity\" from the onion " +#~ "button in the Tor browser's toolbar to clear all history of your " +#~ "SecureDrop usage from this device. If you are not using Tor Browser, " +#~ "restart your browser." +#~ msgstr "" +#~ "您由于超时而被登出。若您想继续使用 SecureDrop 的话请重新登录,或选择洋葱浏" +#~ "览器工具栏的 “新身份” 来清除您在此设备上访问 SecureDrop 的记录。若您使用的" +#~ "不是洋葱浏览器,请重启您的浏览器。" + +#~ msgid "Token in two-factor authentication verified." +#~ msgstr "两步验证码已通过验证。" + +#~ msgid "Could not verify token in two-factor authentication." +#~ msgstr "两步验证码无法通过验证。" + +#~ msgid "Can't scan the barcode? Enter the following code manually:" +#~ msgstr "无法扫描条形码?手动输入条形码的数字:" + +#~ msgid "RESET PASSWORD" +#~ msgstr "重置密码" + +#~ msgid "Change Username & Admin Status" +#~ msgstr "更改账户名称 & 管理员状态" + +#~ msgid "Change username" +#~ msgstr "更改用户名" + +#~ msgid "SecureDrop now uses automatically generated diceware passwords." +#~ msgstr "SecureDrop 现使用虚拟骰子自动生成密码。" + +#~ msgid "" +#~ "Critical Security: The operating system used by your " +#~ "SecureDrop servers has reached its end-of-life. A manual update is " +#~ "urgently required to remain safe - Learn More" +#~ msgstr "" +#~ "安全隐患: 您使用的操作系统已经达到了使用年限,急需手动更" +#~ "新以保持安全 - 进一步了解" + +#~ msgid "Is Administrator" +#~ msgstr "是管理员" + +#~ msgid "Upload images only." +#~ msgstr "仅上传图片。" From e71ae8968bd0093cb02cc121076ea8694ecfa33d Mon Sep 17 00:00:00 2001 From: John Hensley Date: Thu, 7 Jan 2021 20:09:47 -0500 Subject: [PATCH 3/3] Update i18n.py to support better locale selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The way we were naming locales in the selection dropdown, using only part of the language name, wasn't sufficient to distinguish simplified and traditional Chinese. It was also off for Norwegian, in that we were just calling it "Norsk", which isn't the complete language name, indicating which written standard we use (Bokmål). So I've changed it to use the complete language name, and include further distinguishing information as necessary when we have more than one translation for a language. We were also constructing the mapping of locale identifiers to display names every request, but always using the locale endonyms for the display names, instead of showing display names in the visitor's language, so it didn't need to be done per request. I moved the creation of the locale name map to the app setup. Our negotiation of locales using the request's accepted languages was also not working for Chinese, because we store the Chinese translations by language and script, not region, so if a browser indicated acceptable Chinese locales containing regions, they wouldn't be matched and the visitor's first contact would be in English. While cleaning up the locale selector presentation, I consolidated some of the things we were tacking onto the Flask g object into a container class, i18n.RequestLocaleInfo. The source metadata API endpoint could duplicate the default locale in its list of supported languages. That's been fixed. --- securedrop/i18n.py | 301 +++++++++++------- securedrop/journalist_app/__init__.py | 7 +- securedrop/journalist_app/account.py | 3 +- securedrop/journalist_app/admin.py | 5 +- securedrop/journalist_templates/base.html | 2 +- securedrop/journalist_templates/locales.html | 4 +- securedrop/sass/modules/_menu.sass | 12 +- securedrop/sdconfig.py | 8 +- securedrop/source_app/__init__.py | 19 +- securedrop/source_app/main.py | 3 +- securedrop/source_app/utils.py | 3 +- securedrop/source_templates/base.html | 2 +- securedrop/source_templates/index.html | 2 +- securedrop/source_templates/locales.html | 6 +- .../source_templates/session_timeout.html | 4 +- securedrop/tests/test_i18n.py | 163 ++++++---- securedrop/tests/test_journalist.py | 2 + securedrop/tests/test_source.py | 2 + securedrop/tests/test_template_filters.py | 6 +- 19 files changed, 335 insertions(+), 219 deletions(-) diff --git a/securedrop/i18n.py b/securedrop/i18n.py index 68f127caef..b42c8116c3 100644 --- a/securedrop/i18n.py +++ b/securedrop/i18n.py @@ -15,151 +15,222 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # -from flask import Flask -from flask import request, session -from flask_babel import Babel -from babel import core - import collections -import os -import re -from typing import List +from typing import Dict, List -from typing import Dict +from babel.core import ( + Locale, + UnknownLocaleError, + get_locale_identifier, + negotiate_locale, + parse_locale, +) +from flask import Flask, g, request, session +from flask_babel import Babel from sdconfig import SDConfig -LOCALE_SPLIT = re.compile('(-|_)') -LOCALES = ['en_US'] -babel = None +class RequestLocaleInfo: + """ + Convenience wrapper around a babel.core.Locale. + """ -class LocaleNotFound(Exception): + def __init__(self, locale: str): + self.locale = Locale.parse(locale) - """Raised when the desired locale is not in the translations directory""" + def __str__(self) -> str: + """ + The Babel string representation of the locale. + """ + return str(self.locale) + @property + def text_direction(self) -> str: + """ + The Babel text direction: ltr or rtl. + + Used primarily to set text direction in HTML via the "dir" + attribute. + """ + return self.locale.text_direction + + @property + def language(self) -> str: + """ + The Babel language name. + + Just the language, without subtag info like region or script. + """ + return self.locale.language + + @property + def id(self) -> str: + """ + The Babel string representation of the locale. + + This should match the name of the directory containing its + translations. + """ + return str(self.locale) + + @property + def language_tag(self) -> str: + """ + Returns a BCP47/RFC5646 language tag for the locale. + + Language tags are used in HTTP headers and the HTML lang + attribute. + """ + return get_locale_identifier(parse_locale(str(self.locale)), sep="-") -def setup_app(config: SDConfig, app: Flask) -> None: - global LOCALES - global babel - # `babel.translation_directories` is a nightmare - # We need to set this manually via an absolute path - app.config['BABEL_TRANSLATION_DIRECTORIES'] = str(config.TRANSLATION_DIRS.absolute()) +def configure_babel(config: SDConfig, app: Flask) -> None: + """ + Set up Flask-Babel according to the SecureDrop configuration. + """ + # Tell Babel where to find our translations. + translations_directory = str(config.TRANSLATION_DIRS.absolute()) + app.config["BABEL_TRANSLATION_DIRECTORIES"] = translations_directory + # Create the app's Babel instance. Passing the app to the + # constructor causes the instance to attach itself to the app. babel = Babel(app) - if len(list(babel.translation_directories)) != 1: - raise AssertionError( - 'Expected exactly one translation directory but got {}.' - .format(babel.translation_directories)) - - translation_directories = next(babel.translation_directories) - for dirname in os.listdir(translation_directories): - if dirname != 'messages.pot': - LOCALES.append(dirname) - - LOCALES = _get_supported_locales( - LOCALES, - config.SUPPORTED_LOCALES, - config.DEFAULT_LOCALE, - translation_directories) + # verify that Babel is only using the translations we told it about + if list(babel.translation_directories) != [translations_directory]: + raise ValueError( + "Babel translation directories ({}) do not match SecureDrop configuration ({})".format( + babel.translation_directories, [translations_directory] + ) + ) + + # register the function used to determine the locale of a request babel.localeselector(lambda: get_locale(config)) +def validate_locale_configuration(config: SDConfig, app: Flask) -> None: + """ + Ensure that the configured locales are valid and translated. + """ + if config.DEFAULT_LOCALE not in config.SUPPORTED_LOCALES: + raise ValueError( + 'The default locale "{}" is not included in the set of supported locales "{}"'.format( + config.DEFAULT_LOCALE, config.SUPPORTED_LOCALES + ) + ) + + translations = app.babel_instance.list_translations() + for locale in config.SUPPORTED_LOCALES: + if locale == "en_US": + continue + + parsed = Locale.parse(locale) + if parsed not in translations: + raise ValueError( + 'Configured locale "{}" is not in the set of translated locales "{}"'.format( + parsed, translations + ) + ) + + +LOCALES = collections.OrderedDict() # type: collections.OrderedDict[str, str] + + +def map_locale_display_names(config: SDConfig) -> None: + """ + Create a map of locale identifiers to names for display. + + For most of our supported languages, we only provide one + translation, so including the full display name is not necessary + to distinguish them. For languages with more than one translation, + like Chinese, we do need the additional detail. + """ + language_locale_counts = collections.defaultdict(int) # type: Dict[str, int] + for l in sorted(config.SUPPORTED_LOCALES): + locale = Locale.parse(l) + language_locale_counts[locale.language_name] += 1 + + locale_map = collections.OrderedDict() + for l in sorted(config.SUPPORTED_LOCALES): + locale = Locale.parse(l) + if language_locale_counts[locale.language_name] == 1: + name = locale.language_name + else: + name = locale.display_name + locale_map[str(locale)] = name + + global LOCALES + LOCALES = locale_map + + +def configure(config: SDConfig, app: Flask) -> None: + configure_babel(config, app) + validate_locale_configuration(config, app) + map_locale_display_names(config) + + def get_locale(config: SDConfig) -> str: """ + Return the best supported locale for a request. + Get the locale as follows, by order of precedence: - l request argument or session['locale'] - browser suggested locale, from the Accept-Languages header - config.DEFAULT_LOCALE - - 'en_US' """ - accept_languages = [] - for l in list(request.accept_languages.values()): - if '-' in l: - sep = '-' - else: - sep = '_' - try: - accept_languages.append(str(core.Locale.parse(l, sep))) - except Exception: - pass - if 'l' in request.args: - if len(request.args['l']) == 0: - if 'locale' in session: - del session['locale'] - locale = core.negotiate_locale(accept_languages, LOCALES) - else: - locale = core.negotiate_locale([request.args['l']], LOCALES) - session['locale'] = locale - else: - if 'locale' in session: - locale = session['locale'] - else: - locale = core.negotiate_locale(accept_languages, LOCALES) + # Default to any locale set in the session. + locale = session.get("locale") - if locale: - return locale - else: - return config.DEFAULT_LOCALE + # A valid locale specified in request.args takes precedence. + if request.args.get("l"): + negotiated = negotiate_locale([request.args["l"]], LOCALES.keys()) + if negotiated: + locale = negotiated + # If the locale is not in the session or request.args, negotiate + # the best supported option from the browser's accepted languages. + if not locale: + locale = negotiate_locale(get_accepted_languages(), LOCALES.keys()) -def get_text_direction(locale: str) -> str: - return core.Locale.parse(locale).text_direction + # Finally, fall back to the default locale if necessary. + return locale or config.DEFAULT_LOCALE -def _get_supported_locales(locales: List[str], supported: List[str], default_locale: str, - translation_directories: str) -> List[str]: - """Sanity checks on locales and supported locales from config.py. - Return the list of supported locales. +def get_accepted_languages() -> List[str]: """ - - if not supported: - return [default_locale or 'en_US'] - unsupported = set(supported) - set(locales) - if unsupported: - raise LocaleNotFound( - "config.py SUPPORTED_LOCALES contains {} which is not among the " - "locales found in the {} directory: {}".format( - list(unsupported), - translation_directories, - locales)) - if default_locale and default_locale not in supported: - raise LocaleNotFound("config.py SUPPORTED_LOCALES contains {} " - "which does not include " - "the value of DEFAULT_LOCALE '{}'".format( - supported, default_locale)) - - return list(supported) - - -NAME_OVERRIDES = { - 'nb_NO': 'norsk', -} - - -def get_locale2name() -> Dict[str, str]: - locale2name = collections.OrderedDict() - for l in LOCALES: - if l in NAME_OVERRIDES: - locale2name[l] = NAME_OVERRIDES[l] - else: - locale = core.Locale.parse(l) - locale2name[l] = locale.languages[locale.language] - return locale2name - - -def locale_to_rfc_5646(locale: str) -> str: - lower = locale.lower() - if 'hant' in lower: - return 'zh-Hant' - elif 'hans' in lower: - return 'zh-Hans' - else: - return LOCALE_SPLIT.split(locale)[0] + Convert a request's list of accepted languages into locale identifiers. + """ + accept_languages = [] + for l in request.accept_languages.values(): + try: + parsed = Locale.parse(l, "-") + accept_languages.append(str(parsed)) + + # We only have two Chinese translations, simplified + # and traditional, based on script and not + # region. Browsers tend to send identifiers with + # region, e.g. zh-CN or zh-TW. Babel can generally + # infer the script from those, so we can fabricate a + # fallback entry without region, in the hope that it + # will match one of our translations and the site will + # at least be more legible at first contact than the + # probable default locale of English. + if parsed.language == "zh" and parsed.script: + accept_languages.append( + str(Locale(language=parsed.language, script=parsed.script)) + ) + except (ValueError, UnknownLocaleError): + pass + return accept_languages -def get_language(config: SDConfig) -> str: - return get_locale(config).split('_')[0] +def set_locale(config: SDConfig) -> None: + """ + Update locale info in request and session. + """ + locale = get_locale(config) + g.localeinfo = RequestLocaleInfo(locale) + session["locale"] = locale + g.locales = LOCALES diff --git a/securedrop/journalist_app/__init__.py b/securedrop/journalist_app/__init__.py index a1e38dd098..df097f9e92 100644 --- a/securedrop/journalist_app/__init__.py +++ b/securedrop/journalist_app/__init__.py @@ -102,7 +102,7 @@ def _handle_http_exception( for code in default_exceptions: app.errorhandler(code)(_handle_http_exception) - i18n.setup_app(config, app) + i18n.configure(config, app) app.jinja_env.trim_blocks = True app.jinja_env.lstrip_blocks = True @@ -156,10 +156,7 @@ def setup_g() -> 'Optional[Response]': if uid: g.user = Journalist.query.get(uid) - g.locale = i18n.get_locale(config) - g.text_direction = i18n.get_text_direction(g.locale) - g.html_lang = i18n.locale_to_rfc_5646(g.locale) - g.locales = i18n.get_locale2name() + i18n.set_locale(config) if app.instance_config.organization_name: g.organization_name = app.instance_config.organization_name diff --git a/securedrop/journalist_app/account.py b/securedrop/journalist_app/account.py index 35745121d4..1359de8abf 100644 --- a/securedrop/journalist_app/account.py +++ b/securedrop/journalist_app/account.py @@ -6,7 +6,6 @@ flash, session) from flask_babel import gettext -import i18n from db import db from journalist_app.utils import (set_diceware_password, set_name, validate_user, validate_hotp_secret) @@ -20,7 +19,7 @@ def make_blueprint(config: SDConfig) -> Blueprint: @view.route('/account', methods=('GET',)) def edit() -> str: password = PassphraseGenerator.get_default().generate_passphrase( - preferred_language=i18n.get_language(config) + preferred_language=g.localeinfo.language ) return render_template('edit_account.html', password=password) diff --git a/securedrop/journalist_app/admin.py b/securedrop/journalist_app/admin.py index 52287aec9a..7676ae3dba 100644 --- a/securedrop/journalist_app/admin.py +++ b/securedrop/journalist_app/admin.py @@ -11,7 +11,6 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.exc import NoResultFound -import i18n from db import db from html import escape from models import (InstanceConfig, Journalist, InvalidUsernameException, @@ -153,7 +152,7 @@ def add_user() -> Union[str, werkzeug.Response]: uid=new_user.id)) password = PassphraseGenerator.get_default().generate_passphrase( - preferred_language=i18n.get_language(config) + preferred_language=g.localeinfo.language ) return render_template("admin_add_user.html", password=password, @@ -253,7 +252,7 @@ def edit_user(user_id: int) -> Union[str, werkzeug.Response]: commit_account_changes(user) password = PassphraseGenerator.get_default().generate_passphrase( - preferred_language=i18n.get_language(config) + preferred_language=g.localeinfo.language ) return render_template("edit_account.html", user=user, password=password) diff --git a/securedrop/journalist_templates/base.html b/securedrop/journalist_templates/base.html index aa88cc1703..a036b10f9d 100644 --- a/securedrop/journalist_templates/base.html +++ b/securedrop/journalist_templates/base.html @@ -1,5 +1,5 @@ - + diff --git a/securedrop/journalist_templates/locales.html b/securedrop/journalist_templates/locales.html index 89019e4f99..3a075bd7f7 100644 --- a/securedrop/journalist_templates/locales.html +++ b/securedrop/journalist_templates/locales.html @@ -6,7 +6,7 @@