diff --git a/README.md b/README.md index 94ac1d4f9ac776b1912aea20223a9e8858a07e3e..f6969459174c3321a6267eeba10ff08ad43e5bb8 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,16 @@ too. [Install / Self-hosting](https://ntfy.sh/docs/install/) | [Building](https://ntfy.sh/docs/develop/) +## Chat +You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org) +(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information +[on my website](https://heckel.io/about). + +## Announcements / beta testers +For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements) +topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas, +join Discord/Matrix (I'll eventually make a testing channel in Google Play). + ## Contributing I welcome any and all contributions. Just create a PR or an issue. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/) for the server and the Android app. @@ -43,11 +53,6 @@ Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start im <img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" /> </a> -## Contact me -You can directly contact me **[on Discord](https://discord.gg/cT7ECsZj9w)** or [on Matrix](https://matrix.to/#/#ntfy:matrix.org) -(bridged from Discord), or via the [GitHub issues](https://github.com/binwiederhier/ntfy/issues), or find more contact information -[on my website](https://heckel.io/about). - ## License Made with â¤ï¸ by [Philipp C. Heckel](https://heckel.io). The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2). diff --git a/docs/config.md b/docs/config.md index 15b547bb4ffcacfa553703ac1e65b91d841effd5..9b0aa07c2393af6f21b2cdbb747a5cf1b6e4fe25 100644 --- a/docs/config.md +++ b/docs/config.md @@ -643,10 +643,18 @@ In case you're curious, here's an example of the entire flow: - In the iOS app, you subscribe to `https://ntfy.example.com/mytopic` - The app subscribes to the Firebase topic `6de73be8dfb7d69e...` (the SHA256 of the topic URL) - When you publish a message to `https://ntfy.example.com/mytopic`, your ntfy server will publish a - poll request to `https://ntfy.sh/6de73be8dfb7d69e...` (passing the message ID in the `X-Poll-ID` header) -- The ntfy.sh server publishes the message to Firebase, which forwards it to APNS, which forwards it to your iOS device + poll request to `https://ntfy.sh/6de73be8dfb7d69e...`. The request from your server to the upstream server + contains only the message ID (in the `X-Poll-ID` header), and the SHA256 checksum of the topic URL (as upstream topic). +- The ntfy.sh server publishes the poll request message to Firebase, which forwards it to APNS, which forwards it to your iOS device - Your iOS device receives the poll request, and fetches the actual message from your server, and then displays it +Here's an example of what the self-hosted server forwards to the upstream server. The request is equivalent to this curl: + +``` +curl -X POST -H "X-Poll-ID: s4PdJozxM8na" https://ntfy.sh/6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b +{"id":"4HsClFEuCIcs","time":1654087955,"event":"poll_request","topic":"6de73be8dfb7d69e32fb2c00c23fe7adbd8b5504406e3068c273aa24cef4055b","message":"New message","poll_id":"s4PdJozxM8na"} +``` + ## Rate limiting !!! info Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag. diff --git a/docs/install.md b/docs/install.md index 6c18d56fc3535a47605d14a922f4a6d58a0d545c..231b430681dff1705c465c557a4d5c56bca40d4c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -176,6 +176,12 @@ cd ntfysh-bin makepkg -si ``` +## NixOS / Nix +ntfy is packaged in nixpkgs as `ntfy-sh`. It can be installed by adding the package name to the configuration file and calling `nixos-rebuild`. Alternatively, the following command can be used to install ntfy in the current user environment: +``` +nix-env -iA ntfy-sh +``` + ## macOS The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well. To install, please download the tarball, extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`). diff --git a/docs/releases.md b/docs/releases.md index e4f78db9f1c431495cdb3bf9b57a537a803fbaa5..1a3fa4d6064c7ced119c8a762cf24107d7dce7ee 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -25,6 +25,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release **Documentation**: * [Examples](examples.md) for [Home Assistant](https://www.home-assistant.io/) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@poblabs](https://github.com/poblabs)) +* Install instructions for [NixOS/Nix](https://ntfy.sh/docs/install/#nixos-nix) ([#282](https://github.com/binwiederhier/ntfy/pull/282), thanks to [@arjan-s](https://github.com/arjan-s)) +* Clarify `poll_request` wording for [iOS push notifications](https://ntfy.sh/docs/config/#ios-instant-notifications) ([#300](https://github.com/binwiederhier/ntfy/issues/300), thanks to [@prabirshrestha](https://github.com/prabirshrestha) for reporting) + +**Additional translations:** + +* Chinese/Simplified (thanks to [@yufei.im](https://hosted.weblate.org/user/yufei.im/)) --> diff --git a/server/server_firebase_test.go b/server/server_firebase_test.go index 8e08b0d6f859e2e013c05303e213d434975794c2..b29cf3af4dbbbf195e95d9a6431613bb589e7826 100644 --- a/server/server_firebase_test.go +++ b/server/server_firebase_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "heckel.io/ntfy/auth" "strings" + "sync" "testing" ) @@ -29,6 +30,7 @@ func (t testAuther) Authorize(_ *auth.User, _ string, _ auth.Permission) error { type testFirebaseSender struct { allowed int messages []*messaging.Message + mu sync.Mutex } func newTestFirebaseSender(allowed int) *testFirebaseSender { @@ -37,7 +39,10 @@ func newTestFirebaseSender(allowed int) *testFirebaseSender { messages: make([]*messaging.Message, 0), } } + func (s *testFirebaseSender) Send(m *messaging.Message) error { + s.mu.Lock() + defer s.mu.Unlock() if len(s.messages)+1 > s.allowed { return errFirebaseQuotaExceeded } @@ -45,6 +50,12 @@ func (s *testFirebaseSender) Send(m *messaging.Message) error { return nil } +func (s *testFirebaseSender) Messages() []*messaging.Message { + s.mu.Lock() + defer s.mu.Unlock() + return append(make([]*messaging.Message, 0), s.messages...) +} + func TestToFirebaseMessage_Keepalive(t *testing.T) { m := newKeepaliveMessage("mytopic") fbm, err := toFirebaseMessage(m, nil) @@ -311,15 +322,15 @@ func TestToFirebaseSender_Abuse(t *testing.T) { visitor := newVisitor(newTestConfig(t), newMemTestCache(t), "1.2.3.4") require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"})) - require.Equal(t, 1, len(sender.messages)) + require.Equal(t, 1, len(sender.Messages())) require.Nil(t, client.Send(visitor, &message{Topic: "mytopic"})) - require.Equal(t, 2, len(sender.messages)) + require.Equal(t, 2, len(sender.Messages())) require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"})) - require.Equal(t, 2, len(sender.messages)) + require.Equal(t, 2, len(sender.Messages())) sender.messages = make([]*messaging.Message, 0) // Reset to test that time limit is working require.Equal(t, errFirebaseQuotaExceeded, client.Send(visitor, &message{Topic: "mytopic"})) - require.Equal(t, 0, len(sender.messages)) + require.Equal(t, 0, len(sender.Messages())) } diff --git a/server/server_test.go b/server/server_test.go index d05075fdcfa89848ffcfb45509b538e6647c819e..ce63f272ad1e163f3c7874687bf311af0dc83f09 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -63,10 +63,10 @@ func TestServer_PublishWithFirebase(t *testing.T) { msg1 := toMessage(t, response.Body.String()) require.NotEmpty(t, msg1.ID) require.Equal(t, "my first message", msg1.Message) - require.Equal(t, 1, len(sender.messages)) - require.Equal(t, "my first message", sender.messages[0].Data["message"]) - require.Equal(t, "my first message", sender.messages[0].APNS.Payload.Aps.Alert.Body) - require.Equal(t, "my first message", sender.messages[0].APNS.Payload.CustomData["message"]) + require.Equal(t, 1, len(sender.Messages())) + require.Equal(t, "my first message", sender.Messages()[0].Data["message"]) + require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.Aps.Alert.Body) + require.Equal(t, "my first message", sender.Messages()[0].APNS.Payload.CustomData["message"]) } func TestServer_SubscribeOpenAndKeepalive(t *testing.T) { diff --git a/web/public/static/langs/zh_Hans.json b/web/public/static/langs/zh_Hans.json new file mode 100644 index 0000000000000000000000000000000000000000..e084bcf289809a9ce29625baaea5019e57f42554 --- /dev/null +++ b/web/public/static/langs/zh_Hans.json @@ -0,0 +1,191 @@ +{ + "action_bar_show_menu": "显示èœå•", + "action_bar_logo_alt": "ntfyå›¾æ ‡", + "action_bar_settings": "设置", + "action_bar_send_test_notification": "å‘é€æµ‹è¯•é€šçŸ¥", + "action_bar_clear_notifications": "清除所有通知", + "action_bar_unsubscribe": "å–消订阅", + "action_bar_toggle_action_menu": "å¼€å¯æˆ–å…³é—æ“作èœå•", + "message_bar_type_message": "在æ¤å¤„输入消æ¯", + "message_bar_show_dialog": "显示å‘布对è¯æ¡†", + "message_bar_publish": "å‘布消æ¯", + "nav_topics_title": "订阅主题", + "nav_button_all_notifications": "全部通知", + "nav_button_documentation": "文档", + "nav_button_publish_message": "å‘布通知", + "nav_button_subscribe": "订阅主题", + "nav_button_connecting": "æ£åœ¨è¿žæŽ¥", + "alert_grant_title": "å·²ç¦ç”¨é€šçŸ¥", + "alert_grant_description": "授予æµè§ˆå™¨æ˜¾ç¤ºæ¡Œé¢é€šçŸ¥çš„æƒé™ã€‚", + "alert_grant_button": "现在授予", + "alert_not_supported_title": "ä¸æ”¯æŒé€šçŸ¥", + "alert_not_supported_description": "您的æµè§ˆå™¨ä¸æ”¯æŒé€šçŸ¥ã€‚", + "notifications_list": "通知列表", + "notifications_list_item": "通知", + "notifications_mark_read": "æ ‡è®°ä¸ºå·²è¯»", + "notifications_copied_to_clipboard": "å¤åˆ¶åˆ°å‰ªè´´æ¿", + "notifications_tags": "æ ‡è®°", + "notifications_priority_x": "优先级 {{priority}}", + "notifications_new_indicator": "新通知", + "notifications_attachment_open_button": "打开附件", + "notifications_attachment_link_expires": "链接过期 {{date}}", + "notifications_attachment_link_expired": "下载链接已过期", + "notifications_attachment_file_image": "图片文件", + "notifications_attachment_image": "附件图片", + "notifications_attachment_file_video": "视频文件", + "notifications_attachment_file_audio": "音频文件", + "notifications_attachment_file_app": "安å“应用文件", + "notifications_attachment_file_document": "其他文件", + "notifications_click_copy_url_title": "å¤åˆ¶é“¾æŽ¥åœ°å€åˆ°å‰ªè´´æ¿", + "notifications_click_copy_url_button": "å¤åˆ¶é“¾æŽ¥", + "notifications_click_open_button": "打开链接", + "action_bar_toggle_mute": "æš‚åœæˆ–æ¢å¤é€šçŸ¥", + "nav_button_muted": "已暂åœé€šçŸ¥", + "notifications_actions_not_supported": "网页应用程åºä¸æ”¯æŒæ“作", + "notifications_none_for_topic_title": "您尚未收到有关æ¤ä¸»é¢˜çš„任何通知。", + "notifications_none_for_any_title": "您尚未收到任何通知。", + "notifications_none_for_any_description": "è¦å‘æ¤ä¸»é¢˜å‘é€é€šçŸ¥ï¼Œåªéœ€ä½¿ç”¨ PUT 或 POST 到主题链接å³å¯ã€‚以下是使用您的主题的示例。", + "notifications_no_subscriptions_title": "看起æ¥ä½ 还没有任何订阅。", + "notifications_example": "示例", + "notifications_more_details": "有关更多信æ¯ï¼Œè¯·æŸ¥çœ‹<websiteLink>网站</websiteLink>或<docsLink>文档</docsLink>。", + "notifications_loading": "æ£åœ¨åŠ 载通知……", + "publish_dialog_title_topic": "å‘布到 {{topic}}", + "publish_dialog_title_no_topic": "å‘布通知", + "publish_dialog_progress_uploading": "æ£åœ¨ä¸Šä¼ ……", + "publish_dialog_progress_uploading_detail": "æ£åœ¨ä¸Šä¼ {{loaded}}/{{total}} ({{percent}}%) ……", + "publish_dialog_message_published": "å·²å‘布通知", + "publish_dialog_attachment_limits_file_and_quota_reached": "超过 {{fileSizeLimit}} 文件é™åˆ¶å’Œé…é¢ï¼Œå‰©ä½™ {{remainingBytes}}", + "publish_dialog_emoji_picker_show": "选择表情符å·", + "publish_dialog_priority_min": "最低优先级", + "publish_dialog_priority_low": "低优先级", + "publish_dialog_priority_default": "默认优先级", + "publish_dialog_priority_high": "高优先级", + "publish_dialog_priority_max": "最高优先级", + "publish_dialog_topic_label": "主题å称", + "publish_dialog_topic_placeholder": "主题å称,例如 phil_alerts", + "publish_dialog_topic_reset": "é‡ç½®ä¸»é¢˜", + "publish_dialog_title_label": "主题", + "publish_dialog_message_label": "消æ¯", + "publish_dialog_message_placeholder": "在æ¤è¾“入消æ¯", + "publish_dialog_tags_label": "æ ‡è®°", + "publish_dialog_priority_label": "优先级", + "publish_dialog_base_url_label": "æœåŠ¡é“¾æŽ¥åœ°å€", + "publish_dialog_base_url_placeholder": "æœåŠ¡é“¾æŽ¥åœ°å€ï¼Œä¾‹å¦‚ https://example.com", + "publish_dialog_click_label": "点击链接地å€", + "publish_dialog_click_placeholder": "点击通知时打开链接地å€", + "publish_dialog_email_placeholder": "将通知转å‘到的地å€ï¼Œä¾‹å¦‚ phil@example.com", + "publish_dialog_email_reset": "移除电å邮件转å‘", + "publish_dialog_filename_label": "文件å", + "publish_dialog_filename_placeholder": "附件文件å", + "publish_dialog_delay_label": "延期", + "publish_dialog_other_features": "其它功能:", + "publish_dialog_attach_placeholder": "使用链接地å€é™„åŠ æ–‡ä»¶ï¼Œä¾‹å¦‚ https://f-droid.org/F-Droid.apk", + "publish_dialog_delay_reset": "åˆ é™¤å»¶è¿Ÿäº¤ä»˜", + "publish_dialog_attach_reset": "移除附件链接地å€", + "publish_dialog_chip_click_label": "点击链接地å€", + "publish_dialog_chip_email_label": "转å‘邮件", + "publish_dialog_chip_attach_file_label": "本地文件附件", + "publish_dialog_chip_topic_label": "å˜æ›´ä¸»é¢˜", + "publish_dialog_button_cancel_sending": "å–消å‘é€", + "publish_dialog_checkbox_publish_another": "å‘布å¦ä¸€ä¸ª", + "publish_dialog_attached_file_title": "附件文件:", + "publish_dialog_attached_file_filename_placeholder": "附件文件å", + "publish_dialog_attached_file_remove": "åˆ é™¤é™„ä»¶æ–‡ä»¶", + "publish_dialog_drop_file_here": "将文件拖拽至æ¤", + "emoji_picker_search_placeholder": "查找表情符å·", + "emoji_picker_search_clear": "清除æœç´¢", + "subscribe_dialog_subscribe_title": "订阅主题", + "publish_dialog_chip_delay_label": "延迟交付", + "publish_dialog_chip_attach_url_label": "链接附件地å€", + "subscribe_dialog_subscribe_use_another_label": "使用其他æœåŠ¡å™¨", + "subscribe_dialog_subscribe_button_subscribe": "订阅", + "subscribe_dialog_login_title": "请登录", + "subscribe_dialog_login_description": "本主题å—密ç ä¿æŠ¤ï¼Œè¯·è¾“入用户å和密ç 进行订阅。", + "subscribe_dialog_login_username_label": "用户å,例如 phil", + "subscribe_dialog_login_password_label": "密ç ", + "subscribe_dialog_login_button_back": "返回", + "subscribe_dialog_login_button_login": "登录", + "subscribe_dialog_error_user_not_authorized": "æœªæŽˆæƒ {{username}} 用户", + "subscribe_dialog_error_user_anonymous": "匿å", + "prefs_notifications_title": "通知", + "prefs_notifications_sound_title": "通知æ示音", + "prefs_notifications_sound_description_none": "收到通知时ä¸æ’放任何声音", + "prefs_notifications_sound_description_some": "收到通知时æ’放 {{sound}} 声音", + "prefs_notifications_sound_no_sound": "é™éŸ³", + "prefs_notifications_sound_play": "æ’放选ä¸å£°éŸ³", + "prefs_notifications_min_priority_title": "最低优先级", + "prefs_notifications_min_priority_description_x_or_higher": "仅显示优先级为{{number}}({{name}})或以上的通知", + "prefs_notifications_min_priority_description_max": "仅显示最高优先级的通知", + "prefs_notifications_min_priority_any": "ä»»æ„优先级", + "prefs_notifications_min_priority_low_and_higher": "低优先级和更高优先级", + "prefs_notifications_min_priority_default_and_higher": "默认优先级或更高优先级", + "prefs_notifications_min_priority_high_and_higher": "高优先级或更高优先级", + "prefs_notifications_min_priority_max_only": "仅最高优先级", + "prefs_notifications_delete_after_never": "从ä¸", + "prefs_notifications_delete_after_one_month": "一月åŽ", + "prefs_notifications_delete_after_one_week": "一周åŽ", + "prefs_notifications_delete_after_never_description": "æ°¸ä¸è‡ªåŠ¨åˆ 除通知", + "prefs_notifications_delete_after_three_hours_description": "三å°æ—¶åŽè‡ªåŠ¨åˆ 除通知", + "prefs_notifications_delete_after_one_day_description": "一天åŽè‡ªåŠ¨åˆ 除通知", + "prefs_notifications_delete_after_one_week_description": "一周åŽè‡ªåŠ¨åˆ 除通知", + "prefs_notifications_delete_after_one_month_description": "一月åŽåŽè‡ªåŠ¨åˆ 除通知", + "prefs_users_title": "管ç†ç”¨æˆ·", + "prefs_users_description": "在æ¤å¤„æ·»åŠ /åˆ é™¤å—ä¿æŠ¤ä¸»é¢˜çš„用户。请注æ„,用户å和密ç å˜å‚¨åœ¨æµè§ˆå™¨çš„本地å˜å‚¨ä¸ã€‚", + "prefs_users_add_button": "æ·»åŠ ç”¨æˆ·", + "prefs_users_dialog_title_add": "æ·»åŠ ç”¨æˆ·", + "prefs_users_dialog_title_edit": "编辑用户", + "prefs_users_dialog_username_label": "用户å,例如 phil", + "prefs_users_dialog_password_label": "密ç ", + "prefs_users_dialog_button_cancel": "å–消", + "prefs_users_dialog_button_save": "ä¿å˜", + "prefs_appearance_title": "外观", + "prefs_appearance_language_title": "è¯è¨€", + "priority_min": "最低", + "priority_low": "低", + "priority_default": "默认", + "priority_high": "高", + "priority_max": "最高", + "error_boundary_title": "天啊,ntfy 崩溃了", + "prefs_users_table_base_url_header": "æœåŠ¡é“¾æŽ¥åœ°å€", + "prefs_users_dialog_base_url_label": "æœåŠ¡é“¾æŽ¥åœ°å€ï¼Œä¾‹å¦‚ https://ntfy.sh", + "error_boundary_button_copy_stack_trace": "å¤åˆ¶å †æ ˆè·Ÿè¸ª", + "error_boundary_stack_trace": "å †æ ˆè·Ÿè¸ª", + "error_boundary_gathering_info": "收集更多信æ¯â€¦â€¦", + "error_boundary_unsupported_indexeddb_title": "ä¸æ”¯æŒéšç§æµè§ˆ", + "error_boundary_unsupported_indexeddb_description": "Ntfy Web应用程åºéœ€è¦IndexedDBæ‰èƒ½è¿è¡Œï¼Œå¹¶ä¸”您的æµè§ˆå™¨åœ¨ç§éšç§æµè§ˆæ¨¡å¼ä¸‹ä¸æ”¯æŒIndexedDB。<br/><br/>虽然这很ä¸å¹¸ï¼Œä½†åœ¨éšç§æµè§ˆæ¨¡å¼ä¸‹ä½¿ç”¨ntfy Web应用程åºä¹Ÿæ²¡æœ‰å¤šå¤§æ„ä¹‰ï¼Œå› ä¸ºæ‰€æœ‰ä¸œè¥¿éƒ½å˜å‚¨åœ¨æµè§ˆå™¨å˜å‚¨ä¸ã€‚您å¯ä»¥åœ¨<githubLink>本GitHub问题</githubLink>ä¸é˜…读有关它的更多信æ¯ï¼Œæˆ–者在<discordLink>Discord</discordLink>或<matrixLink>Matrix</matrixLink>上与我们交谈。", + "message_bar_error_publishing": "å‘布通知时出错", + "nav_button_settings": "设置", + "notifications_delete": "åˆ é™¤", + "notifications_attachment_copy_url_title": "将附件ä¸é“¾æŽ¥åœ°å€å¤åˆ¶åˆ°å‰ªè´´æ¿", + "notifications_attachment_copy_url_button": "å¤åˆ¶é“¾æŽ¥åœ°å€", + "notifications_attachment_open_title": "转到 {{url}}", + "notifications_actions_http_request_title": "å‘é€ HTTP {{method}} 到 {{url}}", + "notifications_actions_open_url_title": "转到 {{url}}", + "notifications_none_for_topic_description": "è¦å‘æ¤ä¸»é¢˜å‘é€é€šçŸ¥ï¼Œåªéœ€ä½¿ç”¨ PUT 或 POST 到主题链接å³å¯ã€‚", + "subscribe_dialog_subscribe_topic_placeholder": "主题å,例如 phil_alerts", + "notifications_no_subscriptions_description": "å•å‡» \"{{linktext}}\" 链接以创建或订阅主题。之åŽï¼Œæ‚¨å¯ä»¥ä½¿ç”¨ PUT 或 POST å‘é€æ¶ˆæ¯ï¼Œæ‚¨å°†åœ¨è¿™é‡Œæ”¶åˆ°é€šçŸ¥ã€‚", + "publish_dialog_attachment_limits_file_reached": "超过 {{fileSizeLimit}} 文件é™åˆ¶", + "publish_dialog_title_placeholder": "ä¸»é¢˜æ ‡é¢˜ï¼Œä¾‹å¦‚ ç£ç›˜ç©ºé—´å‘Šè¦", + "publish_dialog_email_label": "电å邮件", + "publish_dialog_button_send": "å‘é€", + "publish_dialog_attachment_limits_quota_reached": "超过é…é¢ï¼Œå‰©ä½™ {{remainingBytes}}", + "publish_dialog_attach_label": "附件链接地å€", + "publish_dialog_click_reset": "移除点击连接地å€", + "publish_dialog_button_cancel": "å–消", + "subscribe_dialog_subscribe_button_cancel": "å–消", + "subscribe_dialog_subscribe_base_url_label": "æœåŠ¡åœ°å€åœ°å€", + "prefs_notifications_min_priority_description_any": "æ˜¾ç¤ºæ‰€æœ‰é€šçŸ¥ï¼Œæ— è®ºä¼˜å…ˆçº§å¦‚ä½•", + "prefs_notifications_delete_after_title": "åˆ é™¤é€šçŸ¥", + "prefs_notifications_delete_after_three_hours": "三å°æ—¶åŽ", + "prefs_users_delete_button": "åˆ é™¤ç”¨æˆ·", + "prefs_users_table_user_header": "用户", + "prefs_users_dialog_button_add": "æ·»åŠ ", + "prefs_notifications_delete_after_one_day": "一天åŽ", + "error_boundary_description": "这显然ä¸åº”该å‘生。对æ¤éžå¸¸æŠ±æ‰ã€‚<br/>如果您有时间,请<githubLink>在GitHub</githubLink>上报告,或通过<discordLink>Discord</discordLink>或<matrixLink>Matrix</matrixLink>告诉我们。", + "prefs_users_table": "用户表", + "prefs_users_edit_button": "编辑用户", + "publish_dialog_tags_placeholder": "英文逗å·åˆ†éš”æ ‡è®°åˆ—è¡¨ï¼Œä¾‹å¦‚ warning, srv1-backup", + "publish_dialog_details_examples_description": "有关所有å‘é€åŠŸèƒ½çš„示例和详细说明,请å‚阅<docsLink>文档</docsLink>。", + "subscribe_dialog_subscribe_description": "主题å¯èƒ½ä¸å—密ç ä¿æŠ¤ï¼Œå› æ¤è¯·é€‰æ‹©ä¸€ä¸ªä¸å®¹æ˜“猜测的åå—。订阅åŽï¼Œæ‚¨å¯ä»¥ä½¿ç”¨ PUT/POST 通知。", + "publish_dialog_delay_placeholder": "延迟交付,例如{{unixTimestamp}}ã€{{relativeTime}}或“{{naturalLanguage}}â€ï¼ˆä»…é™è‹±è¯ï¼‰" +} diff --git a/web/src/components/Preferences.js b/web/src/components/Preferences.js index b93702fa7fe837d9c097e67b934d388cf4c43efb..e2899ecce1a9c902205577cfb456910f49f989b9 100644 --- a/web/src/components/Preferences.js +++ b/web/src/components/Preferences.js @@ -436,7 +436,7 @@ const Appearance = () => { const Language = () => { const { t, i18n } = useTranslation(); const labelId = "prefLanguage"; - const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇮🇹", "ðŸ‡ðŸ‡º", "🇧🇷", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3); + const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇫🇷", "🇧🇬", "🇨🇿", "🇩🇪", "🇨🇳", "🇮🇹", "ðŸ‡ðŸ‡º", "🇧🇷", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3); const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" "); const lang = i18n.language ?? "en"; @@ -452,6 +452,7 @@ const Language = () => { <MenuItem value="id">Bahasa Indonesia</MenuItem> <MenuItem value="bg">БългарÑки</MenuItem> <MenuItem value="cs">ÄŒeÅ¡tina</MenuItem> + <MenuItem value="zh_Hans">ä¸æ–‡</MenuItem> <MenuItem value="de">Deutsch</MenuItem> <MenuItem value="es">Español</MenuItem> <MenuItem value="fr">Français</MenuItem>