From f7259f625feb8e19d337d122ff6c8c61ec4b488e Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 9 Jul 2025 04:03:39 -0400 Subject: [PATCH 01/10] Prefer `on: :update` in Tag validation declaration (#35297) --- app/models/tag.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index a3ccdd8ac6..8e21ddca82 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -49,8 +49,8 @@ class Tag < ApplicationRecord validates :name, presence: true, format: { with: HASHTAG_NAME_RE } validates :display_name, format: { with: HASHTAG_NAME_RE } - validate :validate_name_change, if: -> { !new_record? && name_changed? } - validate :validate_display_name_change, if: -> { !new_record? && display_name_changed? } + validate :validate_name_change, on: :update, if: :name_changed? + validate :validate_display_name_change, on: :update, if: :display_name_changed? scope :pending_review, -> { unreviewed.where.not(requested_review_at: nil) } scope :usable, -> { where(usable: [true, nil]) } From fb6c22f5c275685aa644d84c003e1d6922e15d40 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 9 Jul 2025 04:04:00 -0400 Subject: [PATCH 02/10] Use `touch` to record viewing annual report (#35296) --- app/models/generated_annual_report.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/generated_annual_report.rb b/app/models/generated_annual_report.rb index 43c97d7108..aba0712fe4 100644 --- a/app/models/generated_annual_report.rb +++ b/app/models/generated_annual_report.rb @@ -24,7 +24,7 @@ class GeneratedAnnualReport < ApplicationRecord end def view! - update!(viewed_at: Time.now.utc) + touch(:viewed_at) end def account_ids From 1e2d77f2c72c373bf1800a8faa62a8b2966b7afc Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 9 Jul 2025 04:45:29 -0400 Subject: [PATCH 03/10] Use `if_exists: true` when removing duplicate indexes (#35309) --- db/migrate/20241014010506_remove_duplicate_indexes.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/db/migrate/20241014010506_remove_duplicate_indexes.rb b/db/migrate/20241014010506_remove_duplicate_indexes.rb index 50e0e6ffcf..0c71936c43 100644 --- a/db/migrate/20241014010506_remove_duplicate_indexes.rb +++ b/db/migrate/20241014010506_remove_duplicate_indexes.rb @@ -2,9 +2,11 @@ class RemoveDuplicateIndexes < ActiveRecord::Migration[7.1] def change - remove_index :account_aliases, :account_id - remove_index :account_relationship_severance_events, :account_id - remove_index :custom_filter_statuses, :status_id - remove_index :webauthn_credentials, :user_id + with_options if_exists: true do + remove_index :account_aliases, :account_id + remove_index :account_relationship_severance_events, :account_id + remove_index :custom_filter_statuses, :status_id + remove_index :webauthn_credentials, :user_id + end end end From 8bd2c87399eebdb3c345ba1cb1c6a7b6d5b69a9d Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 9 Jul 2025 10:58:41 +0200 Subject: [PATCH 04/10] Fix support for special characters in various environment variables (#35314) Co-authored-by: Matt Jankowski --- config/cache_buster.yml | 4 ++-- config/email.yml | 18 +++++++++--------- config/mastodon.yml | 10 +++++----- config/translation.yml | 6 +++--- config/vapid.yml | 4 ++-- spec/configuration/email_spec.rb | 22 ++++++++++++++++++++++ 6 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 spec/configuration/email_spec.rb diff --git a/config/cache_buster.yml b/config/cache_buster.yml index 709c0eba88..09d6cfc6ea 100644 --- a/config/cache_buster.yml +++ b/config/cache_buster.yml @@ -1,5 +1,5 @@ shared: enabled: <%= ENV.fetch('CACHE_BUSTER_ENABLED', 'false') == 'true' %> - secret_header: <%= ENV.fetch('CACHE_BUSTER_SECRET_HEADER', nil) %> - secret: <%= ENV.fetch('CACHE_BUSTER_SECRET', nil) %> + secret_header: <%= ENV.fetch('CACHE_BUSTER_SECRET_HEADER', nil)&.to_json %> + secret: <%= ENV.fetch('CACHE_BUSTER_SECRET', nil)&.to_json %> http_method: <%= ENV.fetch('CACHE_BUSTER_HTTP_METHOD', 'GET') %> diff --git a/config/email.yml b/config/email.yml index a6d34c3006..f39aa2545a 100644 --- a/config/email.yml +++ b/config/email.yml @@ -2,14 +2,14 @@ # keys are added here. production: delivery_method: <%= ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp') %> - from_address: <%= ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost') %> - reply_to: <%= ENV.fetch('SMTP_REPLY_TO', nil) %> - return_path: <%= ENV.fetch('SMTP_RETURN_PATH', nil) %> + from_address: <%= ENV.fetch('SMTP_FROM_ADDRESS', 'notifications@localhost')&.to_json %> + reply_to: <%= ENV.fetch('SMTP_REPLY_TO', nil)&.to_json %> + return_path: <%= ENV.fetch('SMTP_RETURN_PATH', nil)&.to_json %> smtp_settings: port: <%= ENV.fetch('SMTP_PORT', nil) %> - address: <%= ENV.fetch('SMTP_SERVER', nil) %> - user_name: <%= ENV.fetch('SMTP_LOGIN', nil) %> - password: <%= ENV.fetch('SMTP_PASSWORD', nil) %> + address: <%= ENV.fetch('SMTP_SERVER', nil)&.to_json %> + user_name: <%= ENV.fetch('SMTP_LOGIN', nil)&.to_json %> + password: <%= ENV.fetch('SMTP_PASSWORD', nil)&.to_json %> domain: <%= ENV.fetch('SMTP_DOMAIN', ENV.fetch('LOCAL_DOMAIN', nil)) %> authentication: <%= ENV.fetch('SMTP_AUTH_METHOD', 'plain') %> ca_file: <%= ENV.fetch('SMTP_CA_FILE', '/etc/ssl/certs/ca-certificates.crt') %> @@ -22,9 +22,9 @@ production: bulk_mail: smtp_settings: port: <%= ENV.fetch('BULK_SMTP_PORT', nil) %> - address: <%= ENV.fetch('BULK_SMTP_SERVER', nil) %> - user_name: <%= ENV.fetch('BULK_SMTP_LOGIN', nil) %> - password: <%= ENV.fetch('BULK_SMTP_PASSWORD', nil) %> + address: <%= ENV.fetch('BULK_SMTP_SERVER', nil)&.to_json %> + user_name: <%= ENV.fetch('BULK_SMTP_LOGIN', nil)&.to_json %> + password: <%= ENV.fetch('BULK_SMTP_PASSWORD', nil)&.to_json %> domain: <%= ENV.fetch('BULK_SMTP_DOMAIN', ENV.fetch('LOCAL_DOMAIN', nil)) %> authentication: <%= ENV.fetch('BULK_SMTP_AUTH_METHOD', 'plain') %> ca_file: <%= ENV.fetch('BULK_SMTP_CA_FILE', '/etc/ssl/certs/ca-certificates.crt') %> diff --git a/config/mastodon.yml b/config/mastodon.yml index 31c2b2b785..4585e1f2ae 100644 --- a/config/mastodon.yml +++ b/config/mastodon.yml @@ -2,14 +2,14 @@ shared: experimental_features: <%= ENV.fetch('EXPERIMENTAL_FEATURES', nil) %> limited_federation_mode: <%= (ENV.fetch('LIMITED_FEDERATION_MODE', nil) || ENV.fetch('WHITELIST_MODE', nil)) == 'true' %> - self_destruct_value: <%= ENV.fetch('SELF_DESTRUCT', nil) %> - software_update_url: <%= ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check') %> + self_destruct_value: <%= ENV.fetch('SELF_DESTRUCT', nil)&.to_json %> + software_update_url: <%= ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check')&.to_json %> source: - base_url: <%= ENV.fetch('SOURCE_BASE_URL', nil) %> + base_url: <%= ENV.fetch('SOURCE_BASE_URL', nil)&.to_json %> repository: <%= ENV.fetch('GITHUB_REPOSITORY', 'mastodon/mastodon') %> tag: <%= ENV.fetch('SOURCE_TAG', nil) %> version: - metadata: <%= ENV.fetch('MASTODON_VERSION_METADATA', nil) %> - prerelease: <%= ENV.fetch('MASTODON_VERSION_PRERELEASE', nil) %> + metadata: <%= ENV.fetch('MASTODON_VERSION_METADATA', nil)&.to_json %> + prerelease: <%= ENV.fetch('MASTODON_VERSION_PRERELEASE', nil)&.to_json %> test: experimental_features: <%= [ENV.fetch('EXPERIMENTAL_FEATURES', nil), 'testing_only'].compact.join(',') %> diff --git a/config/translation.yml b/config/translation.yml index e074c5d0f2..75754928ee 100644 --- a/config/translation.yml +++ b/config/translation.yml @@ -1,7 +1,7 @@ shared: deepl: - api_key: <%= ENV.fetch('DEEPL_API_KEY', nil) %> + api_key: <%= ENV.fetch('DEEPL_API_KEY', nil)&.to_json %> plan: <%= ENV.fetch('DEEPL_PLAN', 'free') %> libre_translate: - api_key: <%= ENV.fetch('LIBRE_TRANSLATE_API_KEY', nil) %> - endpoint: <%= ENV.fetch('LIBRE_TRANSLATE_ENDPOINT', nil) %> + api_key: <%= ENV.fetch('LIBRE_TRANSLATE_API_KEY', nil)&.to_json %> + endpoint: <%= ENV.fetch('LIBRE_TRANSLATE_ENDPOINT', nil)&.to_json %> diff --git a/config/vapid.yml b/config/vapid.yml index c3ee806fd6..49d8cd0de8 100644 --- a/config/vapid.yml +++ b/config/vapid.yml @@ -13,5 +13,5 @@ # https://rossta.net/blog/using-the-web-push-api-with-vapid.html # shared: - private_key: <%= ENV.fetch('VAPID_PRIVATE_KEY', nil) %> - public_key: <%= ENV.fetch('VAPID_PUBLIC_KEY', nil) %> + private_key: <%= ENV.fetch('VAPID_PRIVATE_KEY', nil)&.to_json %> + public_key: <%= ENV.fetch('VAPID_PUBLIC_KEY', nil)&.to_json %> diff --git a/spec/configuration/email_spec.rb b/spec/configuration/email_spec.rb new file mode 100644 index 0000000000..2838beb645 --- /dev/null +++ b/spec/configuration/email_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Configuration for email', type: :feature do + context 'with special characters in SMTP_PASSWORD env variable' do + let(:password) { ']]123456789[["!:@<>/\\=' } + + around do |example| + ClimateControl.modify SMTP_PASSWORD: password do + example.run + end + end + + it 'parses value correctly' do + expect(Rails.application.config_for(:email, env: :production)) + .to include( + smtp_settings: include(password: password) + ) + end + end +end From 76c14464161fd7ca295b82b76aeaca4186e17474 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 9 Jul 2025 11:10:55 +0200 Subject: [PATCH 05/10] New Crowdin Translations (automated) (#35310) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/bg.json | 2 +- app/javascript/mastodon/locales/en-GB.json | 24 +++++++ app/javascript/mastodon/locales/fy.json | 3 + app/javascript/mastodon/locales/kab.json | 11 ++++ app/javascript/mastodon/locales/zh-CN.json | 3 + config/locales/activerecord.en-GB.yml | 6 ++ config/locales/br.yml | 3 + config/locales/cs.yml | 4 ++ config/locales/da.yml | 6 +- config/locales/de.yml | 4 ++ config/locales/doorkeeper.cs.yml | 2 +- config/locales/en-GB.yml | 74 ++++++++++++++++++++++ config/locales/es-AR.yml | 4 ++ config/locales/es-MX.yml | 4 ++ config/locales/es.yml | 4 ++ config/locales/eu.yml | 20 ++++++ config/locales/fi.yml | 4 ++ config/locales/fo.yml | 4 ++ config/locales/fy.yml | 11 ++++ config/locales/gl.yml | 4 ++ config/locales/he.yml | 4 ++ config/locales/is.yml | 4 ++ config/locales/kab.yml | 16 +++++ config/locales/ko.yml | 4 ++ config/locales/nl.yml | 4 ++ config/locales/simple_form.br.yml | 1 + config/locales/simple_form.cs.yml | 2 + config/locales/simple_form.da.yml | 2 + config/locales/simple_form.de.yml | 2 + config/locales/simple_form.en-GB.yml | 23 +++++++ config/locales/simple_form.es-AR.yml | 2 + config/locales/simple_form.es-MX.yml | 2 + config/locales/simple_form.es.yml | 2 + config/locales/simple_form.fi.yml | 1 + config/locales/simple_form.fo.yml | 2 + config/locales/simple_form.fy.yml | 2 + config/locales/simple_form.gl.yml | 2 + config/locales/simple_form.he.yml | 2 + config/locales/simple_form.is.yml | 2 + config/locales/simple_form.ko.yml | 2 + config/locales/simple_form.nl.yml | 2 + config/locales/simple_form.tr.yml | 2 + config/locales/simple_form.vi.yml | 2 + config/locales/simple_form.zh-CN.yml | 1 + config/locales/simple_form.zh-TW.yml | 2 + config/locales/tr.yml | 4 ++ config/locales/vi.yml | 4 ++ config/locales/zh-CN.yml | 4 ++ config/locales/zh-TW.yml | 4 ++ 49 files changed, 300 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/locales/bg.json b/app/javascript/mastodon/locales/bg.json index 61951ee236..8628b68954 100644 --- a/app/javascript/mastodon/locales/bg.json +++ b/app/javascript/mastodon/locales/bg.json @@ -188,7 +188,7 @@ "community.column_settings.remote_only": "Само отдалечено", "compose.language.change": "Смяна на езика", "compose.language.search": "Търсене на езици...", - "compose.published.body": "Публикувана публикация.", + "compose.published.body": "Публикувано.", "compose.published.open": "Отваряне", "compose.saved.body": "Запазена публикация.", "compose_form.direct_message_warning_learn_more": "Още информация", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 588576ad0a..1702076b3f 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -219,6 +219,13 @@ "confirmations.delete_list.confirm": "Delete", "confirmations.delete_list.message": "Are you sure you want to permanently delete this list?", "confirmations.delete_list.title": "Delete list?", + "confirmations.discard_draft.confirm": "Discard and continue", + "confirmations.discard_draft.edit.cancel": "Resume editing", + "confirmations.discard_draft.edit.message": "Continuing will discard any changes you have made to the post you are currently editing.", + "confirmations.discard_draft.edit.title": "Discard changes to your post?", + "confirmations.discard_draft.post.cancel": "Resume draft", + "confirmations.discard_draft.post.message": "Continuing will discard the post you are currently composing.", + "confirmations.discard_draft.post.title": "Discard your draft post?", "confirmations.discard_edit_media.confirm": "Discard", "confirmations.discard_edit_media.message": "You have unsaved changes to the media description or preview, discard them anyway?", "confirmations.follow_to_list.confirm": "Follow and add to list", @@ -330,6 +337,7 @@ "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.report_issue": "Report issue", "explore.suggested_follows": "People", + "explore.title": "Trending", "explore.trending_links": "News", "explore.trending_statuses": "Posts", "explore.trending_tags": "Hashtags", @@ -541,8 +549,10 @@ "mute_modal.you_wont_see_mentions": "You won't see posts that mention them.", "mute_modal.you_wont_see_posts": "They can still see your posts, but you won't see theirs.", "navigation_bar.about": "About", + "navigation_bar.account_settings": "Password and security", "navigation_bar.administration": "Administration", "navigation_bar.advanced_interface": "Open in advanced web interface", + "navigation_bar.automated_deletion": "Automated post deletion", "navigation_bar.blocks": "Blocked users", "navigation_bar.bookmarks": "Bookmarks", "navigation_bar.direct": "Private mentions", @@ -552,13 +562,23 @@ "navigation_bar.follow_requests": "Follow requests", "navigation_bar.followed_tags": "Followed hashtags", "navigation_bar.follows_and_followers": "Follows and followers", + "navigation_bar.import_export": "Import and export", "navigation_bar.lists": "Lists", + "navigation_bar.live_feed_local": "Live feed (local)", + "navigation_bar.live_feed_public": "Live feed (public)", "navigation_bar.logout": "Logout", "navigation_bar.moderation": "Moderation", + "navigation_bar.more": "More", "navigation_bar.mutes": "Muted users", "navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.", "navigation_bar.preferences": "Preferences", + "navigation_bar.privacy_and_reach": "Privacy and reach", "navigation_bar.search": "Search", + "navigation_bar.search_trends": "Search / Trending", + "navigation_panel.collapse_followed_tags": "Collapse followed hashtags menu", + "navigation_panel.collapse_lists": "Collapse list menu", + "navigation_panel.expand_followed_tags": "Expand followed hashtags menu", + "navigation_panel.expand_lists": "Expand list menu", "not_signed_in_indicator.not_signed_in": "You need to sign in to access this resource.", "notification.admin.report": "{name} reported {target}", "notification.admin.report_account": "{name} reported {count, plural, one {one post} other {# posts}} from {target} for {category}", @@ -785,6 +805,7 @@ "report_notification.categories.violation": "Rule violation", "report_notification.categories.violation_sentence": "rule violation", "report_notification.open": "Open report", + "search.clear": "Clear search", "search.no_recent_searches": "No recent searches", "search.placeholder": "Search", "search.quick_action.account_search": "Profiles matching {x}", @@ -887,7 +908,10 @@ "subscribed_languages.save": "Save changes", "subscribed_languages.target": "Change subscribed languages for {target}", "tabs_bar.home": "Home", + "tabs_bar.menu": "Menu", "tabs_bar.notifications": "Notifications", + "tabs_bar.publish": "New Post", + "tabs_bar.search": "Search", "terms_of_service.effective_as_of": "Effective as of {date}", "terms_of_service.title": "Terms of Service", "terms_of_service.upcoming_changes_on": "Upcoming changes on {date}", diff --git a/app/javascript/mastodon/locales/fy.json b/app/javascript/mastodon/locales/fy.json index 85d06d063c..33c6d89d19 100644 --- a/app/javascript/mastodon/locales/fy.json +++ b/app/javascript/mastodon/locales/fy.json @@ -564,6 +564,8 @@ "navigation_bar.follows_and_followers": "Folgers en folgjenden", "navigation_bar.import_export": "Ymportearje en eksportearje", "navigation_bar.lists": "Listen", + "navigation_bar.live_feed_local": "Livefeed (lokaal)", + "navigation_bar.live_feed_public": "Livefeed (iepenbier)", "navigation_bar.logout": "Ofmelde", "navigation_bar.moderation": "Moderaasje", "navigation_bar.more": "Mear", @@ -803,6 +805,7 @@ "report_notification.categories.violation": "Skeinde regels", "report_notification.categories.violation_sentence": "skeinde regels", "report_notification.open": "Rapport iepenje", + "search.clear": "Sykopdracht wiskje", "search.no_recent_searches": "Gjin resinte sykopdrachten", "search.placeholder": "Sykje", "search.quick_action.account_search": "Accounts dy’t oerienkomme mei {x}", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index d825c87894..dce8fc460a 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -6,6 +6,7 @@ "about.domain_blocks.preamble": "Maṣṭudun s umata yeḍmen-ak ad teẓreḍ agbur, ad tesdemreḍ akked yimseqdacen-nniḍen seg yal aqeddac deg fedivers. Ha-tent-an ɣur-k tsuraf i yellan deg uqeddac-agi.", "about.domain_blocks.silenced.title": "Ɣur-s talast", "about.domain_blocks.suspended.title": "Yettwaḥbes", + "about.language_label": "Tutlayt", "about.not_available": "Talɣut-a ur tettwabder ara deg uqeddac-a.", "about.powered_by": "Azeṭṭa inmetti yettwasɣelsen sɣur {mastodon}", "about.rules": "Ilugan n uqeddac", @@ -237,6 +238,10 @@ "explore.trending_links": "Isallen", "explore.trending_statuses": "Tisuffaɣ", "explore.trending_tags": "Ihacṭagen", + "featured_carousel.next": "Uḍfiṛ", + "featured_carousel.post": "Tasuffeɣt", + "featured_carousel.previous": "Uzwir", + "featured_carousel.slide": "{index} ɣef {total}", "filter_modal.added.review_and_configure_title": "Iɣewwaṛen n imzizdig", "filter_modal.added.settings_link": "asebter n yiɣewwaṛen", "filter_modal.added.short_explanation": "Tasuffeɣt-a tettwarna ɣer taggayt-a n yimsizdegen: {title}.", @@ -357,6 +362,7 @@ "lists.add_to_list": "Rnu ɣer tebdart", "lists.add_to_lists": "Rnu {name} ɣer tebdarin", "lists.create": "Snulfu-d", + "lists.create_list": "Snulfu-d tabdart", "lists.delete": "Kkes tabdart", "lists.done": "Immed", "lists.edit": "Ẓreg tabdart", @@ -397,6 +403,7 @@ "navigation_bar.lists": "Tibdarin", "navigation_bar.logout": "Ffeɣ", "navigation_bar.moderation": "Aseɣyed", + "navigation_bar.more": "Ugar", "navigation_bar.mutes": "Iseqdacen yettwasusmen", "navigation_bar.opened_in_classic_interface": "Tisuffaɣ, imiḍanen akked isebtar-nniḍen igejdanen ldin-d s wudem amezwer deg ugrudem web aklasiki.", "navigation_bar.preferences": "Imenyafen", @@ -610,6 +617,7 @@ "status.mute_conversation": "Sgugem adiwenni", "status.open": "Semɣeṛ tasuffeɣt-ayi", "status.pin": "Senteḍ-itt deg umaɣnu", + "status.quote_post_author": "Izen sɣur {name}", "status.read_more": "Issin ugar", "status.reblog": "Bḍu", "status.reblogged_by": "Yebḍa-tt {name}", @@ -635,7 +643,10 @@ "status.unpin": "Kkes asenteḍ seg umaɣnu", "subscribed_languages.save": "Sekles ibeddilen", "tabs_bar.home": "Agejdan", + "tabs_bar.menu": "Umuɣ", "tabs_bar.notifications": "Ilɣa", + "tabs_bar.publish": "Tasuffeɣt tamaynut", + "tabs_bar.search": "Nadi", "terms_of_service.title": "Tiwtilin n useqdec", "time_remaining.days": "Mazal {number, plural, one {# wass} other {# wussan}}", "time_remaining.hours": "Mazal {number, plural, one {# usarag} other {# yisragen}}", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index dc34246fb2..57958076a7 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -218,6 +218,9 @@ "confirmations.delete_list.confirm": "删除", "confirmations.delete_list.message": "确定要永久删除此列表吗?", "confirmations.delete_list.title": "确定要删除列表?", + "confirmations.discard_draft.confirm": "放弃并继续", + "confirmations.discard_draft.edit.cancel": "恢复编辑", + "confirmations.discard_draft.post.cancel": "恢复草稿", "confirmations.discard_edit_media.confirm": "丢弃", "confirmations.discard_edit_media.message": "你还有未保存的媒体描述或预览修改,仍要丢弃吗?", "confirmations.follow_to_list.confirm": "关注并添加到列表", diff --git a/config/locales/activerecord.en-GB.yml b/config/locales/activerecord.en-GB.yml index 4e7f7b3e20..fe10c5c8a7 100644 --- a/config/locales/activerecord.en-GB.yml +++ b/config/locales/activerecord.en-GB.yml @@ -49,8 +49,14 @@ en-GB: attributes: reblog: taken: of post already exists + terms_of_service: + attributes: + effective_date: + too_soon: is too soon, must be later than %{date} user: attributes: + date_of_birth: + below_limit: is below the age limit email: blocked: uses a disallowed e-mail provider unreachable: does not seem to exist diff --git a/config/locales/br.yml b/config/locales/br.yml index 9f0cf07ab9..e5b9ff2559 100644 --- a/config/locales/br.yml +++ b/config/locales/br.yml @@ -396,6 +396,9 @@ br: created_at: Deiziad title_actions: none: Diwall + emoji_styles: + auto: Emgefreek + twemoji: Twemoji exports: archive_takeout: date: Deiziad diff --git a/config/locales/cs.yml b/config/locales/cs.yml index e4c083bf70..1ad8df6e71 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -1387,6 +1387,10 @@ cs: basic_information: Základní informace hint_html: "Nastavte si, co lidé uvidí na vašem veřejném profilu a vedle vašich příspěvků. Ostatní lidé vás budou spíše sledovat a komunikovat s vámi, když budete mít vyplněný profil a profilový obrázek." other: Další + emoji_styles: + auto: Auto + native: Výchozí + twemoji: Twemoji errors: '400': Žádost, kterou jste odeslali, byla neplatná nebo poškozená. '403': Nejste oprávněni tuto stránku zobrazit. diff --git a/config/locales/da.yml b/config/locales/da.yml index db3bb547fe..adb59158de 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -1006,7 +1006,7 @@ da: one: Send %{display_count} e-mail other: Send %{display_count} e-mails title: Forhåndsvis Tjenestevilkår-underretning - publish: Udgiv + publish: Publicér published_on_html: Udgivet pr. %{date} save_draft: Gem udkast title: Tjenestevilkår @@ -1349,6 +1349,10 @@ da: basic_information: Oplysninger hint_html: "Tilpas hvad folk ser på din offentlige profil og ved siden af dine indlæg. Andre personer vil mere sandsynligt følge dig tilbage og interagere med dig, når du har en udfyldt profil og et profilbillede." other: Andre + emoji_styles: + auto: Auto + native: Indbygget + twemoji: Twemoji errors: '400': Din indsendte anmodning er ugyldig eller fejlbehæftet. '403': Du har ikke tilladelse til at se denne side. diff --git a/config/locales/de.yml b/config/locales/de.yml index 39530f6ee3..59ceea8e79 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1349,6 +1349,10 @@ de: basic_information: Allgemeine Informationen hint_html: "Bestimme, was andere auf deinem öffentlichen Profil und neben deinen Beiträgen sehen können. Wenn du ein Profilbild festlegst und dein Profil vervollständigst, werden andere eher mit dir interagieren und dir folgen." other: Andere + emoji_styles: + auto: Automatisch + native: Nativ + twemoji: Twemoji errors: '400': Die Anfrage, die du gestellt hast, war ungültig oder fehlerhaft. '403': Dir fehlt die Berechtigung, diese Seite aufzurufen. diff --git a/config/locales/doorkeeper.cs.yml b/config/locales/doorkeeper.cs.yml index 1ee73c2cb9..b435624334 100644 --- a/config/locales/doorkeeper.cs.yml +++ b/config/locales/doorkeeper.cs.yml @@ -29,7 +29,7 @@ cs: edit: title: Upravit aplikaci form: - error: A jéje! Zkontrolujte ve formuláři případné chyby + error: A jéje! Zkontrolujte formulář pro případné chyby help: native_redirect_uri: Pro místní testy použijte %{native_redirect_uri} redirect_uri: Jedno URI na každý řádek diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index f44404502e..a65ee02131 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -309,6 +309,7 @@ en-GB: title: Audit log unavailable_instance: "(domain name unavailable)" announcements: + back: Back to announcements destroyed_msg: Announcement successfully deleted! edit: title: Edit announcement @@ -317,6 +318,10 @@ en-GB: new: create: Create announcement title: New announcement + preview: + disclaimer: As users cannot opt out of them, email notifications should be limited to important announcements such as personal data breach or server closure notifications. + explanation_html: 'The email will be sent to %{display_count} users. The following text will be included in the e-mail:' + title: Preview announcement notification publish: Publish published_msg: Announcement successfully published! scheduled_for: Scheduled for %{time} @@ -475,6 +480,36 @@ en-GB: new: title: Import domain blocks no_file: No file selected + fasp: + debug: + callbacks: + created_at: Created at + delete: Delete + ip: IP address + request_body: Request body + title: Debug Callbacks + providers: + active: Active + base_url: Base URL + callback: Callback + delete: Delete + edit: Edit Provider + finish_registration: Finish registration + name: Name + providers: Providers + public_key_fingerprint: Public key fingerprint + registration_requested: Registration requested + registrations: + confirm: Confirm + description: You received a registration from a FASP. Reject it if you did not initiate this. If you initiated this, carefully compare name and key fingerprint before confirming the registration. + reject: Reject + title: Confirm FASP Registration + save: Save + select_capabilities: Select Capabilities + sign_in: Sign In + status: Status + title: Fediverse Auxiliary Service Providers + title: FASP follow_recommendations: description_html: "Follow recommendations help new users quickly find interesting content. When a user has not interacted with others enough to form personalised follow recommendations, these accounts are recommended instead. They are re-calculated on a daily basis from a mix of accounts with the highest recent engagements and highest local follower counts for a given language." language: For language @@ -543,6 +578,13 @@ en-GB: all: All limited: Limited title: Moderation + moderation_notes: + create: Add Moderation Note + created_msg: Instance moderation note successfully created! + description_html: View and leave notes for other moderators and your future self + destroyed_msg: Instance moderation note successfully deleted! + placeholder: Information about this instance, actions taken, or anything else that will help you moderate this instance in the future. + title: Moderation Notes private_comment: Private comment public_comment: Public comment purge: Purge @@ -751,17 +793,26 @@ en-GB: title: Roles rules: add_new: Add rule + add_translation: Add translation delete: Delete description_html: While most claim to have read and agree to the terms of service, usually people do not read through until after a problem arises. Make it easier to see your server's rules at a glance by providing them in a flat bullet point list. Try to keep individual rules short and simple, but try not to split them up into many separate items either. edit: Edit rule empty: No server rules have been defined yet. + move_down: Move down + move_up: Move up title: Server rules + translation: Translation + translations: Translations + translations_explanation: You can optionally add translations for the rules. The default value will be shown if no translated version is available. Please always ensure any provided translation is in sync with the default value. settings: about: manage_rules: Manage server rules preamble: Provide in-depth information about how the server is operated, moderated, funded. rules_hint: There is a dedicated area for rules that your users are expected to adhere to. title: About + allow_referrer_origin: + desc: When your users click links to external sites, their browser may send the address of your Mastodon server as the referrer. Disable this if this would uniquely identify your users, e.g. if this is a personal Mastodon server. + title: Allow external sites to see your Mastodon server as a traffic source appearance: preamble: Customise Mastodon's web interface. title: Appearance @@ -781,6 +832,7 @@ en-GB: discovery: follow_recommendations: Follow recommendations preamble: Surfacing interesting content is instrumental in onboarding new users who may not know anyone Mastodon. Control how various discovery features work on your server. + privacy: Privacy profile_directory: Profile directory public_timelines: Public timelines publish_statistics: Publish statistics @@ -867,6 +919,8 @@ en-GB: system_checks: database_schema_check: message_html: There are pending database migrations. Please run them to ensure the application behaves as expected + elasticsearch_analysis_index_mismatch: + message_html: Elasticsearch index analyser settings are outdated. Please run tootctl search deploy --only-mapping --only=%{value} elasticsearch_health_red: message_html: Elasticsearch cluster is unhealthy (red status), search features are unavailable elasticsearch_health_yellow: @@ -938,6 +992,7 @@ en-GB: chance_to_review_html: "The generated terms of service will not be published automatically. You will have a chance to review the results. Please fill in the necessary details to proceed." explanation_html: The terms of service template provided is for informational purposes only, and should not be construed as legal advice on any subject matter. Please consult with your own legal counsel on your situation and specific legal questions you have. title: Terms of Service Setup + going_live_on_html: Live, effective %{date} history: History live: Live no_history: There are no recorded changes of the terms of service yet. @@ -1294,6 +1349,10 @@ en-GB: basic_information: Basic information hint_html: "Customise what people see on your public profile and next to your posts. Other people are more likely to follow you back and interact with you when you have a filled out profile and a profile picture." other: Other + emoji_styles: + auto: Auto + native: Native + twemoji: Twemoji errors: '400': The request you submitted was invalid or malformed. '403': You don't have permission to view this page. @@ -1819,6 +1878,10 @@ en-GB: limit: You have already pinned the maximum number of posts ownership: Someone else's post cannot be pinned reblog: A boost cannot be pinned + quote_policies: + followers: Followers and mentioned users + nobody: Only mentioned users + public: Everyone title: '%{name}: "%{quote}"' visibilities: direct: Direct @@ -1872,6 +1935,11 @@ en-GB: does_not_match_previous_name: does not match the previous name terms_of_service: title: Terms of Service + terms_of_service_interstitial: + future_preamble_html: We're making some changes to our terms of service, which will be effective on %{date}. We encourage you to review the updated terms. + past_preamble_html: We have changed our terms of service since your last visit. We encourage you to review the updated terms. + review_link: Review terms of service + title: The terms of service of %{domain} are changing themes: contrast: Mastodon (High contrast) default: Mastodon (Dark) @@ -1903,6 +1971,10 @@ en-GB: recovery_instructions_html: If you ever lose access to your phone, you can use one of the recovery codes below to regain access to your account. Keep the recovery codes safe. For example, you may print them and store them with other important documents. webauthn: Security keys user_mailer: + announcement_published: + description: 'The administrators of %{domain} are making an announcement:' + subject: Service announcement + title: "%{domain} service announcement" appeal_approved: action: Account Settings explanation: The appeal of the strike against your account on %{strike_date} that you submitted on %{appeal_date} has been approved. Your account is once again in good standing. @@ -1935,6 +2007,8 @@ en-GB: terms_of_service_changed: agreement: By continuing to use %{domain}, you are agreeing to these terms. If you disagree with the updated terms, you may terminate your agreement with %{domain} at any time by deleting your account. changelog: 'At a glance, here is what this update means for you:' + description: 'You are receiving this e-mail because we''re making some changes to our terms of service at %{domain}. These updates will take effect on %{date}. We encourage you to review the updated terms in full here:' + description_html: You are receiving this e-mail because we're making some changes to our terms of service at %{domain}. These updates will take effect on %{date}. We encourage you to review the updated terms in full here. sign_off: The %{domain} team subject: Updates to our terms of service subtitle: The terms of service of %{domain} are changing diff --git a/config/locales/es-AR.yml b/config/locales/es-AR.yml index 9ed1661616..9ed961db34 100644 --- a/config/locales/es-AR.yml +++ b/config/locales/es-AR.yml @@ -1349,6 +1349,10 @@ es-AR: basic_information: Información básica hint_html: "Personalizá lo que la gente ve en tu perfil público y junto a tus publicaciones. Es más probable que otras personas te sigan e interactúen con vos cuando tengas un perfil completo y una foto de perfil." other: Otros + emoji_styles: + auto: Automático + native: Nativo + twemoji: Twemoji errors: '400': La solicitud que enviaste no era válida o estaba corrompida. '403': No tenés permiso para ver esta página. diff --git a/config/locales/es-MX.yml b/config/locales/es-MX.yml index e50f2a7f00..cfc573fab8 100644 --- a/config/locales/es-MX.yml +++ b/config/locales/es-MX.yml @@ -1349,6 +1349,10 @@ es-MX: basic_information: Información básica hint_html: "Personaliza lo que la gente ve en tu perfil público junto a tus publicaciones. Es más probable que otras personas te sigan e interactúen contigo cuando completes tu perfil y agregues una foto." other: Otro + emoji_styles: + auto: Automático + native: Nativo + twemoji: Twemoji errors: '400': La solicitud que has enviado no es valida o estaba malformada. '403': No tienes permiso para acceder a esta página. diff --git a/config/locales/es.yml b/config/locales/es.yml index 19f340cec2..4b10f23b13 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1349,6 +1349,10 @@ es: basic_information: Información básica hint_html: "Personaliza lo que la gente ve en tu perfil público junto a tus publicaciones. Es más probable que otras personas te sigan e interactúen contigo cuando completas tu perfil y foto." other: Otros + emoji_styles: + auto: Automático + native: Nativo + twemoji: Twemoji errors: '400': La solicitud que has enviado no es valida o estaba malformada. '403': No tienes permiso para acceder a esta página. diff --git a/config/locales/eu.yml b/config/locales/eu.yml index 19a6be4a9c..bf52c83bc9 100644 --- a/config/locales/eu.yml +++ b/config/locales/eu.yml @@ -187,6 +187,7 @@ eu: create_domain_block: Sortu domeinu blokeoa create_email_domain_block: Sortu email domeinu blokeoa create_ip_block: Sortu IP araua + create_relay: Erreleboa sortu create_unavailable_domain: Sortu eskuragarri ez dagoen domeinua create_user_role: Sortu rola demote_user: Jaitsi erabiltzailearen maila @@ -198,11 +199,13 @@ eu: destroy_email_domain_block: Ezabatu email domeinu blokeoa destroy_instance: Ezabatu betiko domeinua destroy_ip_block: Ezabatu IP araua + destroy_relay: Erreleboa ezabatu destroy_status: Ezabatu bidalketa destroy_unavailable_domain: Ezabatu eskuragarri ez dagoen domeinua destroy_user_role: Ezabatu rola disable_2fa_user: Desgaitu 2FA disable_custom_emoji: Desgaitu emoji pertsonalizatua + disable_relay: Erreleboa desaktibatu disable_sign_in_token_auth_user: Desgaitu email token autentifikazioa erabiltzailearentzat disable_user: Desgaitu erabiltzailea enable_custom_emoji: Gaitu emoji pertsonalizatua @@ -210,6 +213,7 @@ eu: enable_user: Gaitu erabiltzailea memorialize_account: Bihurtu kontua oroigarri promote_user: Igo erabiltzailea mailaz + publish_terms_of_service: Zerbitzu-baldintzak argitaratu reject_appeal: Baztertu apelazioa reject_user: Baztertu erabiltzailea remove_avatar_user: Kendu abatarra @@ -229,23 +233,29 @@ eu: update_custom_emoji: Eguneratu emoji pertsonalizatua update_domain_block: Eguneratu domeinu-blokeoa update_ip_block: Eguneratu IP araua + update_report: Txostena eguneratu update_status: Eguneratu bidalketa update_user_role: Eguneratu rola actions: approve_appeal_html: "%{name} erabiltzaileak %{target} erabiltzailearen moderazio erabakiaren apelazioa onartu du" approve_user_html: "%{name} erabiltzaileak %{target} erabiltzailearen izen-ematea onartu du" assigned_to_self_report_html: "%{name} erabiltzaileak %{target} salaketa bere buruari esleitu dio" + change_email_user_html: "%{name}(e)k %{target} erabiltzailearen e-posta helbidea aldatu du" change_role_user_html: "%{name} erabiltzaileak %{target} kontuaren rola aldatu du" + confirm_user_html: "%{name}(e)k %{target} erabiltzailearen e-posta helbidea berretsi du" create_account_warning_html: "%{name} erabiltzaileak abisua bidali dio %{target} erabiltzaileari" create_announcement_html: "%{name} erabiltzaileak %{target} iragarpen berria sortu du" + create_canonical_email_block_html: "%{name}(e)k %{target} hash-a duen helbide elektronikoa blokeatu du" create_custom_emoji_html: "%{name} erabiltzaileak %{target} emoji berria kargatu du" create_domain_allow_html: "%{name} erabiltzaileak %{target} domeinuarekin federazioa onartu du" create_domain_block_html: "%{name} erabiltzaileak %{target} domeinua blokeatu du" + create_email_domain_block_html: "%{name}(e)k %{target} e-posta helbideen domeinua blokeatu du" create_ip_block_html: "%{name} kontuak %{target} IParen araua sortu du" create_unavailable_domain_html: "%{name}(e)k %{target} domeinurako banaketa gelditu du" create_user_role_html: "%{name} erabiltzaileak %{target} rola sortu du" demote_user_html: "%{name} erabiltzaileak %{target} erabiltzailea mailaz jaitsi du" destroy_announcement_html: "%{name} erabiltzaileak %{target} iragarpena ezabatu du" + destroy_canonical_email_block_html: "%{name}(e)k %{target} hash-a duen helbide elektronikoa desblokeatu du" destroy_custom_emoji_html: "%{name} erabiltzaileak %{target} emoji-a ezabatu du" destroy_domain_allow_html: "%{name} erabiltzaileak %{target} domeinuarekin federatzea debekatu du" destroy_domain_block_html: "%{name} erabiltzaileak %{target} domeinua desblokeatu du" @@ -286,7 +296,9 @@ eu: filter_by_action: Iragazi ekintzen arabera filter_by_user: Iragazi erabiltzaileen arabera title: Auditoria-egunkaria + unavailable_instance: "(domeinu izena ez dago erabilgarri)" announcements: + back: Oharretara bueltatu destroyed_msg: Iragarpena ongi ezabatu da! edit: title: Editatu iragarpena @@ -433,8 +445,10 @@ eu: new: create: Gehitu domeinua resolve: Ebatzi domeinua + title: Posta domeinu berria blokeatu not_permitted: Baimendu gabea resolved_through_html: "%{domain} domeinuaren bidez ebatzia" + title: Email domeinua blokeatuta export_domain_allows: new: title: Baimendutako domeinuak inportatu @@ -471,6 +485,8 @@ eu: save: Gorde sign_in: Hasi saioa status: Egoera + title: Fedibertsoko zerbitzu osagarrien hornitzaileak + title: FZOH follow_recommendations: description_html: "Jarraitzeko gomendioek erabiltzaile berriei eduki interesgarria azkar aurkitzen laguntzen diete. Erabiltzaile batek jarraitzeko gomendio pertsonalizatuak jasotzeko adina interakzio izan ez duenean, kontu hauek gomendatzen zaizkio. Egunero birkalkulatzen dira hizkuntza bakoitzerako, azken aldian parte-hartze handiena izan duten eta jarraitzaile lokal gehien dituzten kontuak nahasiz." language: Hizkuntza @@ -536,6 +552,10 @@ eu: all: Denak limited: Mugatua title: Moderazioa + moderation_notes: + create: Gehitu Moderazio Oharra + created_msg: Instantziako moderazio oharra ongi sortu da! + description_html: Ikusi eta idatzi oharrak beste moderatzaileentzat eta zuretzat etorkizunerako private_comment: Iruzkin pribatua public_comment: Iruzkin publikoa purge: Ezabatu betiko diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 4de6815a00..54aadf60f3 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1347,6 +1347,10 @@ fi: basic_information: Perustiedot hint_html: "Mukauta, mitä ihmiset näkevät julkisessa profiilissasi ja julkaisujesi vieressä. Sinua seurataan takaisin ja kanssasi ollaan vuorovaikutuksessa todennäköisemmin, kun sinulla on täytetty profiili ja profiilikuva." other: Muut + emoji_styles: + auto: Automaattinen + native: Natiivi + twemoji: Twemoji errors: '400': Lähettämäsi pyyntö oli virheellinen tai muotoiltu virheellisesti. '403': Sinulla ei ole oikeutta nähdä tätä sivua. diff --git a/config/locales/fo.yml b/config/locales/fo.yml index 0542719252..23a6b58976 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -1349,6 +1349,10 @@ fo: basic_information: Grundleggjandi upplýsingar hint_html: "Tillaga tað, sum fólk síggja á tínum almenna vanga og við síðna av tínum postum. Sannlíkindini fyri, at onnur fylgja tær og virka saman við tær eru størri, tá tú hevur fylt út vangan og eina vangamynd." other: Onnur + emoji_styles: + auto: Sjálvvirkandi + native: Upprunalig + twemoji: Twitter kenslutekn errors: '400': Umbønin, sum tú sendi inn, var ógildug ella hevði skeivt skap. '403': Tú hevur ikki loyvi at síggja hesa síðuna. diff --git a/config/locales/fy.yml b/config/locales/fy.yml index 561b50079b..38752869da 100644 --- a/config/locales/fy.yml +++ b/config/locales/fy.yml @@ -578,6 +578,13 @@ fy: all: Alle limited: Beheind title: Moderaasje + moderation_notes: + create: Moderaasje-opmerking oanmeitsje + created_msg: Oanmeitsjen fan servermoderaasje-opmerking slagge! + description_html: Opmerkingen besjen en foar josele en oare moderatoaren efterlitte + destroyed_msg: Fuortsmiten fan servermoderaasje-opmerking slagge! + placeholder: Ynformaasje oer dizze server, nommen aksjes of wat oars dy’t jo helpe kinne om dizze server yn de takomst te moderearjen. + title: Moderaasje-opmerkingen private_comment: Priveeopmerking public_comment: Iepenbiere opmerking purge: Folslein fuortsmite @@ -1342,6 +1349,10 @@ fy: basic_information: Algemiene ynformaasje hint_html: "Pas oan wat minsken op jo iepenbiere profyl en njonken jo berjochten sjogge. Oare minsken sille jo earder folgje en mei jo kommunisearje wannear’t jo profyl ynfolle is en jo in profylfoto hawwe." other: Oars + emoji_styles: + auto: Automatysk + native: Systeemeigen + twemoji: Twemoji errors: '400': De oanfraach dy’t jo yntsjinne hawwe wie ûnjildich of fout. '403': Jo hawwe gjin tastimming om dizze side te besjen. diff --git a/config/locales/gl.yml b/config/locales/gl.yml index cfc72681a9..d19d47ab26 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -1349,6 +1349,10 @@ gl: basic_information: Información básica hint_html: "Personaliza o que van ver no teu perfil público e ao lado das túas publicacións. As outras persoas estarán máis animadas a seguirte e interactuar contigo se engades algún dato sobre ti así como unha imaxe de perfil." other: Outros + emoji_styles: + auto: Auto + native: Nativo + twemoji: Twemoji errors: '400': A solicitude que enviou non é válida ou ten formato incorrecto. '403': Non ten permiso para ver esta páxina. diff --git a/config/locales/he.yml b/config/locales/he.yml index 5a562f8116..5071138764 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -1387,6 +1387,10 @@ he: basic_information: מידע בסיסי hint_html: "התאמה אישית של מה שיראו אחרים בפרופיל הציבורי שלך וליד הודעותיך. אחרים עשויים יותר להחזיר עוקב וליצור אתך שיחה אם הפרופיל והתמונה יהיו מלאים." other: אחר + emoji_styles: + auto: אוטומטי + native: מקומי + twemoji: Twemoji errors: '400': הבקשה שהגשת לא תקינה. '403': חסרות לך הרשאות לצפיה בעמוד זה. diff --git a/config/locales/is.yml b/config/locales/is.yml index e997be21b2..5e33189cfb 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -1353,6 +1353,10 @@ is: basic_information: Grunnupplýsingar hint_html: "Sérsníddu hvað fólk sér á opinbera notandasniðinu þínu og næst færslunum þínum. Annað fólk er líklegra til að fylgjast með þér og eiga í samskiptum við þig ef þú fyllir út notandasniðið og setur auðkennismynd." other: Annað + emoji_styles: + auto: Sjálfvirkt + native: Innbyggt + twemoji: Twemoji errors: '400': Beiðnin sem þú sendir er ógild eða rangt uppsett. '403': Þú hefur ekki heimildir til að skoða þessari síðu. diff --git a/config/locales/kab.yml b/config/locales/kab.yml index a7ad9b356f..e68c99e2f8 100644 --- a/config/locales/kab.yml +++ b/config/locales/kab.yml @@ -280,6 +280,7 @@ kab: registrations: confirm: Sentem save: Sekles + sign_in: Qqen title: FASP follow_recommendations: language: I tutlayt @@ -383,6 +384,9 @@ kab: updated_at: Yettwaleqqem view_profile: Wali amaɣnu roles: + assigned_users: + one: "%{count} n useqdac" + other: "%{count} n iseqdacen" categories: administration: Tadbelt invites: Iɛeṛṛuḍen @@ -395,16 +399,20 @@ kab: view_dashboard: Timẓriwt n tfelwit rules: add_new: Rnu alugen + add_translation: Ad yernu tasuqilt delete: Kkes edit: Ẓreg alugen empty: Mazal ur ttwasbadun ara yilugan n uqeddac. title: Ilugan n uqeddac + translation: Tasuqilt + translations: Tisuqilin settings: about: title: Ɣef appearance: title: Udem discovery: + privacy: Tabaḍnit profile_directory: Akaram n imaɣnuten title: Asnirem trends: Ayen mucaɛen @@ -440,6 +448,10 @@ kab: visibility: Abani with_media: S umidya system_checks: + elasticsearch_preset: + action: Wali tasemlit + elasticsearch_preset_single_node: + action: Wali tasemlit rules_check: action: Sefrek ilugan n uqeddac software_version_critical_check: @@ -601,6 +613,8 @@ kab: basic_information: Talɣut tamatut hint_html: "Mudd udem i wayen ttwalin medden deg umaɣnu-inek azayez ɣer yidis n yiznan-ik. Imdanen niḍen zemren ad k-ḍefren yernu ad gen assaɣ yid-k mi ara tesɛuḍ amaɣnu yeccuṛen ed tugna n umaɣnu." other: Ayen nniḍen + emoji_styles: + auto: Awurman errors: '404': Asebter i tettnadiḍ ulac-it da. '500': @@ -835,6 +849,8 @@ kab: one: "%{count} n tbidyutt" other: "%{count} n tbidyutin" edited_at_html: Tettwaẓreg ass n %{date} + quote_policies: + public: Yal yiwen title: '%{name} : "%{quote}"' visibilities: direct: Usrid diff --git a/config/locales/ko.yml b/config/locales/ko.yml index cc6665c1cb..66c16c899d 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1332,6 +1332,10 @@ ko: basic_information: 기본 정보 hint_html: "사람들이 공개 프로필을 보고서 게시물을 볼 때를 위한 프로필을 꾸밉니다. 프로필과 프로필 사진을 채우면 다른 사람들이 나를 팔로우하고 나와 교류할 기회가 더 많아집니다." other: 기타 + emoji_styles: + auto: 자동 + native: 시스템 기본 + twemoji: 트웨모지 errors: '400': 제출한 요청이 올바르지 않습니다. '403': 이 페이지를 표시할 권한이 없습니다. diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 92f2f4ab34..e38d80b100 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1349,6 +1349,10 @@ nl: basic_information: Algemene informatie hint_html: "Wat mensen op jouw openbare profiel en naast je berichten zien aanpassen. Andere mensen gaan je waarschijnlijk eerder volgen en hebben vaker interactie met je, wanneer je profiel is ingevuld en je een profielfoto hebt." other: Overige + emoji_styles: + auto: Auto + native: Systeemeigen + twemoji: Twemoji errors: '400': De aanvraag die je hebt ingediend was ongeldig of foutief. '403': Jij hebt geen toestemming om deze pagina te bekijken. diff --git a/config/locales/simple_form.br.yml b/config/locales/simple_form.br.yml index f4d442cfda..b415640eb0 100644 --- a/config/locales/simple_form.br.yml +++ b/config/locales/simple_form.br.yml @@ -49,6 +49,7 @@ br: setting_display_media_default: Dre ziouer setting_display_media_hide_all: Kuzhat pep tra setting_display_media_show_all: Diskouez pep tra + setting_emoji_style: Stil an emojioù setting_theme: Neuz al lec'hienn setting_use_pending_items: Mod gorrek title: Titl diff --git a/config/locales/simple_form.cs.yml b/config/locales/simple_form.cs.yml index 7af8c1ce7c..eb8f7f2f3a 100644 --- a/config/locales/simple_form.cs.yml +++ b/config/locales/simple_form.cs.yml @@ -61,6 +61,7 @@ cs: setting_display_media_default: Skrývat média označená jako citlivá setting_display_media_hide_all: Vždy skrývat média setting_display_media_show_all: Vždy zobrazovat média + setting_emoji_style: Jak se budou zobrazovat emoji. "Auto" zkusí použít výchozí emoji, ale pro starší prohlížeče použije Twemoji. setting_system_scrollbars_ui: Platí pouze pro desktopové prohlížeče založené na Safari nebo Chrome setting_use_blurhash: Gradienty jsou vytvořeny na základě barvev skrytých médií, ale zakrývají veškeré detaily setting_use_pending_items: Aktualizovat časovou osu až po kliknutí namísto automatického rolování kanálu @@ -243,6 +244,7 @@ cs: setting_display_media_default: Výchozí setting_display_media_hide_all: Skrýt vše setting_display_media_show_all: Zobrazit vše + setting_emoji_style: Styl emoji setting_expand_spoilers: Vždy rozbalit příspěvky označené varováními o obsahu setting_hide_network: Skrýt mou síť setting_missing_alt_text_modal: Zobrazit potvrzovací dialog před odesláním médií bez alt textu diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml index 7ac8816d91..07d36ae1a1 100644 --- a/config/locales/simple_form.da.yml +++ b/config/locales/simple_form.da.yml @@ -61,6 +61,7 @@ da: setting_display_media_default: Skjul medier med sensitiv-markering setting_display_media_hide_all: Skjul altid medier setting_display_media_show_all: Vis altid medier + setting_emoji_style: Hvordan emojis skal vises. "Auto" vil forsøge at bruge indbyggede emojis, men skifter tilbage til Twemoji for ældre browsere. setting_system_scrollbars_ui: Gælder kun for computerwebbrowsere baseret på Safari og Chrome setting_use_blurhash: Gradienter er baseret på de skjulte grafikelementers farver, men slører alle detaljer setting_use_pending_items: Skjul tidslinjeopdateringer bag et klik i stedet for brug af auto-feedrulning @@ -241,6 +242,7 @@ da: setting_display_media_default: Standard setting_display_media_hide_all: Skjul alle setting_display_media_show_all: Vis alle + setting_emoji_style: Emoji-stil setting_expand_spoilers: Udvid altid indlæg markeret med indholdsadvarsler setting_hide_network: Skjul din sociale graf setting_missing_alt_text_modal: Vis bekræftelsesdialog inden medier uden alt-tekst lægges op diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 9f65a474b0..07862f3d88 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -61,6 +61,7 @@ de: setting_display_media_default: Medien mit Inhaltswarnung ausblenden setting_display_media_hide_all: Medien immer ausblenden setting_display_media_show_all: Medien mit Inhaltswarnung immer anzeigen + setting_emoji_style: Darstellung von Emojis. „Automatisch“ verwendet native Emojis, für ältere Browser jedoch Twemoji. setting_system_scrollbars_ui: Betrifft nur Desktop-Browser, die auf Chrome oder Safari basieren setting_use_blurhash: Der Farbverlauf basiert auf den Farben der ausgeblendeten Medien, verschleiert aber jegliche Details setting_use_pending_items: Neue Beiträge hinter einem Klick verstecken, anstatt automatisch zu scrollen @@ -241,6 +242,7 @@ de: setting_display_media_default: Standard setting_display_media_hide_all: Alle Medien ausblenden setting_display_media_show_all: Alle Medien anzeigen + setting_emoji_style: Emoji-Stil setting_expand_spoilers: Beiträge mit Inhaltswarnung immer ausklappen setting_hide_network: Follower und „Folge ich“ nicht anzeigen setting_missing_alt_text_modal: Bestätigungsdialog anzeigen, bevor Medien ohne Bildbeschreibung veröffentlicht werden diff --git a/config/locales/simple_form.en-GB.yml b/config/locales/simple_form.en-GB.yml index 5f5453edc3..fe4160f386 100644 --- a/config/locales/simple_form.en-GB.yml +++ b/config/locales/simple_form.en-GB.yml @@ -56,10 +56,12 @@ en-GB: scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. setting_aggregate_reblogs: Do not show new boosts for posts that have been recently boosted (only affects newly-received boosts) setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon + setting_default_quote_policy: Mentioned users are always allowed to quote. This setting will only take effect for posts created with the next Mastodon version, but you can select your preference in preparation setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click setting_display_media_default: Hide media marked as sensitive setting_display_media_hide_all: Always hide media setting_display_media_show_all: Always show media + setting_emoji_style: How to display emojis. "Auto" will try using native emoji, but falls back to Twemoji for legacy browsers. setting_system_scrollbars_ui: Applies only to desktop browsers based on Safari and Chrome setting_use_blurhash: Gradients are based on the colors of the hidden visuals but obfuscate any details setting_use_pending_items: Hide timeline updates behind a click instead of automatically scrolling the feed @@ -75,6 +77,7 @@ en-GB: filters: action: Chose which action to perform when a post matches the filter actions: + blur: Hide media behind a warning, without hiding the text itself hide: Completely hide the filtered content, behaving as if it did not exist warn: Hide the filtered content behind a warning mentioning the filter's title form_admin_settings: @@ -88,6 +91,7 @@ en-GB: favicon: WEBP, PNG, GIF or JPG. Overrides the default Mastodon favicon with a custom icon. mascot: Overrides the illustration in the advanced web interface. media_cache_retention_period: Media files from posts made by remote users are cached on your server. When set to a positive value, media will be deleted after the specified number of days. If the media data is requested after it is deleted, it will be re-downloaded, if the source content is still available. Due to restrictions on how often link preview cards poll third-party sites, it is recommended to set this value to at least 14 days, or link preview cards will not be updated on demand before that time. + min_age: Users will be asked to confirm their date of birth during sign-up peers_api_enabled: A list of domain names this server has encountered in the fediverse. No data is included here about whether you federate with a given server, just that your server knows about it. This is used by services that collect statistics on federation in a general sense. profile_directory: The profile directory lists all users who have opted-in to be discoverable. require_invite_text: When sign-ups require manual approval, make the “Why do you want to join?” text input mandatory rather than optional @@ -132,14 +136,23 @@ en-GB: name: You can only change the casing of the letters, for example, to make it more readable terms_of_service: changelog: Can be structured with Markdown syntax. + effective_date: A reasonable timeframe can range anywhere from 10 to 30 days from the date you notify your users. text: Can be structured with Markdown syntax. terms_of_service_generator: admin_email: Legal notices include counternotices, court orders, takedown requests, and law enforcement requests. + arbitration_address: Can be the same as Physical address above, or “N/A” if using email. + arbitration_website: Can be a web form, or “N/A” if using email. + choice_of_law: City, region, territory or state the internal substantive laws of which shall govern any and all claims. dmca_address: For US operators, use the address registered in the DMCA Designated Agent Directory. A P.O. Box listing is available upon direct request, use the DMCA Designated Agent Post Office Box Waiver Request to email the Copyright Office and describe that you are a home-based content moderator who fears revenge or retribution for your actions and need to use a P.O. Box to remove your home address from public view. + dmca_email: Can be the same email used for “Email address for legal notices” above. domain: Unique identification of the online service you are providing. jurisdiction: List the country where whoever pays the bills lives. If it’s a company or other entity, list the country where it’s incorporated, and the city, region, territory or state as appropriate. + min_age: Should not be below the minimum age required by the laws of your jurisdiction. user: chosen_languages: When checked, only posts in selected languages will be displayed in public timelines + date_of_birth: + one: We have to make sure you're at least %{count} to use Mastodon. We won't store this. + other: We have to make sure you're at least %{count} to use Mastodon. We won't store this. role: The role controls which permissions the user has. user_role: color: Color to be used for the role throughout the UI, as RGB in hex format @@ -220,6 +233,7 @@ en-GB: setting_boost_modal: Show confirmation dialogue before boosting setting_default_language: Posting language setting_default_privacy: Posting privacy + setting_default_quote_policy: Who can quote setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialogue before deleting a post setting_disable_hover_cards: Disable profile preview on hover @@ -228,6 +242,7 @@ en-GB: setting_display_media_default: Default setting_display_media_hide_all: Hide all setting_display_media_show_all: Show all + setting_emoji_style: Emoji style setting_expand_spoilers: Always expand posts marked with content warnings setting_hide_network: Hide your social graph setting_missing_alt_text_modal: Show confirmation dialogue before posting media without alt text @@ -252,6 +267,7 @@ en-GB: name: Hashtag filters: actions: + blur: Hide media with a warning hide: Hide completely warn: Hide with a warning form_admin_settings: @@ -265,6 +281,7 @@ en-GB: favicon: Favicon mascot: Custom mascot (legacy) media_cache_retention_period: Media cache retention period + min_age: Minimum age requirement peers_api_enabled: Publish list of discovered servers in the API profile_directory: Enable profile directory registrations_mode: Who can sign-up @@ -330,16 +347,22 @@ en-GB: usable: Allow posts to use this hashtag locally terms_of_service: changelog: What's changed? + effective_date: Effective date text: Terms of Service terms_of_service_generator: admin_email: Email address for legal notices arbitration_address: Physical address for arbitration notices arbitration_website: Website for submitting arbitration notices + choice_of_law: Choice of Law dmca_address: Physical address for DMCA/copyright notices dmca_email: Email address for DMCA/copyright notices domain: Domain jurisdiction: Legal jurisdiction + min_age: Minimum age user: + date_of_birth_1i: Day + date_of_birth_2i: Month + date_of_birth_3i: Year role: Role time_zone: Time Zone user_role: diff --git a/config/locales/simple_form.es-AR.yml b/config/locales/simple_form.es-AR.yml index 3b68cf009f..8364741080 100644 --- a/config/locales/simple_form.es-AR.yml +++ b/config/locales/simple_form.es-AR.yml @@ -61,6 +61,7 @@ es-AR: setting_display_media_default: Ocultar medios marcados como sensibles setting_display_media_hide_all: Siempre ocultar todos los medios setting_display_media_show_all: Siempre mostrar todos los medios + setting_emoji_style: Cómo se mostrarán los emojis. "Automático" intentará usar emojis nativos, cambiando a Twemoji en navegadores antiguos. setting_system_scrollbars_ui: Solo aplica para navegadores web de escritorio basados en Safari y Chrome setting_use_blurhash: Los gradientes se basan en los colores de las imágenes ocultas pero haciendo borrosos los detalles setting_use_pending_items: Ocultar actualizaciones de la línea temporal detrás de un clic en lugar de desplazar automáticamente el flujo @@ -241,6 +242,7 @@ es-AR: setting_display_media_default: Predeterminada setting_display_media_hide_all: Ocultar todo setting_display_media_show_all: Mostrar todo + setting_emoji_style: Estilo de emoji setting_expand_spoilers: Siempre expandir los mensajes marcados con advertencias de contenido setting_hide_network: Ocultá tu gráfica social setting_missing_alt_text_modal: Mostrar diálogo de confirmación antes de enviar medios sin texto alternativo diff --git a/config/locales/simple_form.es-MX.yml b/config/locales/simple_form.es-MX.yml index 30614992fc..883f7070e0 100644 --- a/config/locales/simple_form.es-MX.yml +++ b/config/locales/simple_form.es-MX.yml @@ -61,6 +61,7 @@ es-MX: setting_display_media_default: Ocultar contenido multimedia marcado como sensible setting_display_media_hide_all: Siempre ocultar todo el contenido multimedia setting_display_media_show_all: Mostrar siempre contenido multimedia marcado como sensible + setting_emoji_style: Cómo se mostrarán los emojis. "Auto" intentará usar emojis nativos, cambiando a Twemoji en navegadores antiguos. setting_system_scrollbars_ui: Solo se aplica a los navegadores de escritorio basados en Safari y Chrome setting_use_blurhash: Los degradados se basan en los colores de los elementos visuales ocultos, pero ocultan cualquier detalle setting_use_pending_items: Ocultar las publicaciones de la línea de tiempo tras un clic en lugar de desplazar automáticamente el feed @@ -241,6 +242,7 @@ es-MX: setting_display_media_default: Por defecto setting_display_media_hide_all: Ocultar todo setting_display_media_show_all: Mostrar todo + setting_emoji_style: Estilo de emoji setting_expand_spoilers: Siempre expandir las publicaciones marcadas con advertencias de contenido setting_hide_network: Ocultar tu red setting_missing_alt_text_modal: Mostrar cuadro de diálogo de confirmación antes de publicar contenido multimedia sin texto alternativo diff --git a/config/locales/simple_form.es.yml b/config/locales/simple_form.es.yml index 56eea49d25..9c929a8c67 100644 --- a/config/locales/simple_form.es.yml +++ b/config/locales/simple_form.es.yml @@ -61,6 +61,7 @@ es: setting_display_media_default: Ocultar contenido multimedia marcado como sensible setting_display_media_hide_all: Siempre ocultar todo el contenido multimedia setting_display_media_show_all: Mostrar siempre contenido multimedia marcado como sensible + setting_emoji_style: Cómo se mostrarán los emojis. "Auto" intentará usar emojis nativos, cambiando a Twemoji en navegadores antiguos. setting_system_scrollbars_ui: Solo aplica para navegadores de escritorio basados en Safari y Chrome setting_use_blurhash: Los gradientes se basan en los colores de las imágenes ocultas pero haciendo borrosos los detalles setting_use_pending_items: Ocultar nuevas publicaciones detrás de un clic en lugar de desplazar automáticamente el feed @@ -241,6 +242,7 @@ es: setting_display_media_default: Por defecto setting_display_media_hide_all: Ocultar todo setting_display_media_show_all: Mostrar todo + setting_emoji_style: Estilo de emoji setting_expand_spoilers: Siempre expandir las publicaciones marcadas con advertencias de contenido setting_hide_network: Ocultar tu red setting_missing_alt_text_modal: Mostrar diálogo de confirmación antes de publicar medios sin texto alternativo diff --git a/config/locales/simple_form.fi.yml b/config/locales/simple_form.fi.yml index 82f47cd6f3..cafc9e0c7e 100644 --- a/config/locales/simple_form.fi.yml +++ b/config/locales/simple_form.fi.yml @@ -240,6 +240,7 @@ fi: setting_display_media_default: Oletus setting_display_media_hide_all: Piilota kaikki setting_display_media_show_all: Näytä kaikki + setting_emoji_style: Emojityyli setting_expand_spoilers: Laajenna aina sisältövaroituksilla merkityt julkaisut setting_hide_network: Piilota verkostotietosi setting_missing_alt_text_modal: Näytä vahvistusikkuna ennen kuin julkaistaan mediaa ilman vaihtoehtoista tekstiä diff --git a/config/locales/simple_form.fo.yml b/config/locales/simple_form.fo.yml index c3f1dad678..1b5931135c 100644 --- a/config/locales/simple_form.fo.yml +++ b/config/locales/simple_form.fo.yml @@ -61,6 +61,7 @@ fo: setting_display_media_default: Fjal miðlafílur, sum eru merktar sum viðkvæmar setting_display_media_hide_all: Fjal altíð miðlafílur setting_display_media_show_all: Vís altíð miðlafílur + setting_emoji_style: Hvussu kenslutekn vera víst. "Sjálvvirkandi" roynir at brúka upprunalig kenslutekn, men fellir aftur á Twitter kenslutekn í eldri kagum. setting_system_scrollbars_ui: Er einans viðkomandi fyri skriviborðskagar grundaðir á Safari og Chrome setting_use_blurhash: Gradientar eru grundaðir á litirnar av fjaldu myndunum, men grugga allar smálutir setting_use_pending_items: Fjal tíðarlinjudagføringar aftan fyri eitt klikk heldur enn at skrulla tilføringina sjálvvirkandi @@ -241,6 +242,7 @@ fo: setting_display_media_default: Sjálvvirði setting_display_media_hide_all: Fjal alt setting_display_media_show_all: Vís alt + setting_emoji_style: Kensluteknsstílur setting_expand_spoilers: Víðka altíð postar, sum eru merktir við innihaldsávaringum setting_hide_network: Fjal sosiala grafin hjá tær setting_missing_alt_text_modal: Spyr um góðkenning áðrenn miðlar uttan alternativan tekst verða postaðir diff --git a/config/locales/simple_form.fy.yml b/config/locales/simple_form.fy.yml index eea259ef75..ddbac08b73 100644 --- a/config/locales/simple_form.fy.yml +++ b/config/locales/simple_form.fy.yml @@ -61,6 +61,7 @@ fy: setting_display_media_default: As gefoelich markearre media ferstopje setting_display_media_hide_all: Media altyd ferstopje setting_display_media_show_all: Media altyd toane + setting_emoji_style: Wêrmei moatte emojis werjûn wurde. ‘Automatysk’ probearret de systeemeigen emojis te brûken, mar falt werom op Twemoji foar âldere browsers. setting_system_scrollbars_ui: Allinnich fan tapassing op desktopbrowsers basearre op Safari en Chromium setting_use_blurhash: Dizige kleuroergongen binne basearre op de kleuren fan de ferstoppe media, wêrmei elk detail ferdwynt setting_use_pending_items: De tiidline wurdt bywurke troch op it oantal nije items te klikken, yn stee fan dat dizze automatysk bywurke wurdt @@ -241,6 +242,7 @@ fy: setting_display_media_default: Standert setting_display_media_hide_all: Alles ferstopje setting_display_media_show_all: Alles toane + setting_emoji_style: Emojistyl setting_expand_spoilers: Berjochten mei ynhâldswarskôgingen altyd útklappe setting_hide_network: Jo folgers en wa’t jo folget ferstopje setting_missing_alt_text_modal: Befêstigingsfinster toane foar it pleatsen fan media sûnder alt-tekst diff --git a/config/locales/simple_form.gl.yml b/config/locales/simple_form.gl.yml index a425a10384..40354650cc 100644 --- a/config/locales/simple_form.gl.yml +++ b/config/locales/simple_form.gl.yml @@ -61,6 +61,7 @@ gl: setting_display_media_default: Ocultar medios marcados como sensibles setting_display_media_hide_all: Ocultar sempre os medios setting_display_media_show_all: Mostrar sempre os medios marcados como sensibles + setting_emoji_style: Forma de mostrar emojis. «Auto» intentará usar os emojis nativos, e se falla recurrirase a Twemoji en navegadores antigos. setting_system_scrollbars_ui: Aplícase só en navegadores de escritorio baseados en Safari e Chrome setting_use_blurhash: Os gradientes toman as cores da imaxe oculta pero esvaecendo tódolos detalles setting_use_pending_items: Agochar actualizacións da cronoloxía tras un click no lugar de desprazar automáticamente os comentarios @@ -241,6 +242,7 @@ gl: setting_display_media_default: Por defecto setting_display_media_hide_all: Ocultar todo setting_display_media_show_all: Mostrar todo + setting_emoji_style: Estilo dos emojis setting_expand_spoilers: Despregar sempre as publicacións marcadas con avisos de contido setting_hide_network: Non mostrar contactos setting_missing_alt_text_modal: Mostrar mensaxe de confirmación antes de publicar multimedia sen texto descritivo diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml index 4bceaa7e8f..69699c4429 100644 --- a/config/locales/simple_form.he.yml +++ b/config/locales/simple_form.he.yml @@ -61,6 +61,7 @@ he: setting_display_media_default: הסתרת מדיה המסומנת כרגישה setting_display_media_hide_all: הסתר מדיה תמיד setting_display_media_show_all: גלה מדיה תמיד + setting_emoji_style: כיצד להציג רגישונים. "אוטומטי" ינסה להציג מסט האימוג'י המקומי, אבל נופל לערכת Twemoji כברירת מחדל עבור דפדפנים ישנים. setting_system_scrollbars_ui: נוגע רק לגבי דפדפני דסקטופ מבוססים ספארי וכרום setting_use_blurhash: הגראדיינטים מבוססים על תוכן התמונה המוסתרת, אבל מסתירים את כל הפרטים setting_use_pending_items: הסתר עדכוני פיד מאחורי קליק במקום לגלול את הפיד אוטומטית @@ -243,6 +244,7 @@ he: setting_display_media_default: ברירת מחדל setting_display_media_hide_all: להסתיר הכל setting_display_media_show_all: להציג הכול + setting_emoji_style: סגנון רגישונים (אמוג'י) setting_expand_spoilers: להרחיב תמיד הודעות מסומנות באזהרת תוכן setting_hide_network: להחביא את הגרף החברתי שלך setting_missing_alt_text_modal: הצג כרטיס אישור לפני פרסום קובץ גרפי ללא תיאור מילולי diff --git a/config/locales/simple_form.is.yml b/config/locales/simple_form.is.yml index 778d2d8f64..0313bd378b 100644 --- a/config/locales/simple_form.is.yml +++ b/config/locales/simple_form.is.yml @@ -61,6 +61,7 @@ is: setting_display_media_default: Fela myndefni sem merkt er viðkvæmt setting_display_media_hide_all: Alltaf fela allt myndefni setting_display_media_show_all: Alltaf birta myndefni sem merkt er viðkvæmt + setting_emoji_style: Hvernig birta skal tjáningartákn (emoji). "Sjálfvirkt" mun reyna að nota innbyggð tjáningartákn, en til vara verða notuð Twemoji-tákn fyrir eldri vafra. setting_system_scrollbars_ui: Á einungis við um vafra fyrir vinnutölvur sem byggjast á Safari og Chrome setting_use_blurhash: Litstiglarnir byggja á litunum í földu myndunum, en gera öll smáatriði óskýr setting_use_pending_items: Fela uppfærslur tímalínu þar til smellt er, í stað þess að hún skruni streyminu sjálfvirkt @@ -241,6 +242,7 @@ is: setting_display_media_default: Sjálfgefið setting_display_media_hide_all: Fela allt setting_display_media_show_all: Birta allt + setting_emoji_style: Stíll tjáningartákna setting_expand_spoilers: Alltaf útfella færslur sem eru með aðvörun vegna efnisins setting_hide_network: Fela félagsnetið þitt setting_missing_alt_text_modal: Birta staðfestingarglugga áður en myndefni án ALT-hjálpartexta er birt diff --git a/config/locales/simple_form.ko.yml b/config/locales/simple_form.ko.yml index b198ddd0d9..e425bab9fb 100644 --- a/config/locales/simple_form.ko.yml +++ b/config/locales/simple_form.ko.yml @@ -61,6 +61,7 @@ ko: setting_display_media_default: 민감함으로 표시된 미디어 가리기 setting_display_media_hide_all: 모든 미디어를 가리기 setting_display_media_show_all: 모든 미디어를 보이기 + setting_emoji_style: 에모지 표현 방식. "자동"은 시스템 기본 에모지를 적용하고 그렇지 못하는 오래된 브라우저의 경우 트웨모지를 사용합니다. setting_system_scrollbars_ui: 사파리와 크롬 기반의 데스크탑 브라우저만 적용됩니다 setting_use_blurhash: 그라디언트는 숨겨진 내용의 색상을 기반으로 하지만 상세 내용은 보이지 않게 합니다 setting_use_pending_items: 타임라인의 새 게시물을 자동으로 보여 주는 대신, 클릭해서 나타내도록 합니다 @@ -240,6 +241,7 @@ ko: setting_display_media_default: 기본 setting_display_media_hide_all: 모두 가리기 setting_display_media_show_all: 모두 보이기 + setting_emoji_style: 에모지 스타일 setting_expand_spoilers: 내용 경고로 표시된 게시물을 항상 펼치기 setting_hide_network: 내 인맥 숨기기 setting_missing_alt_text_modal: 대체 텍스트 없이 미디어를 게시하려고 할 때 확인창을 띄웁니다 diff --git a/config/locales/simple_form.nl.yml b/config/locales/simple_form.nl.yml index dd9e2f78ca..ef5b2a00b6 100644 --- a/config/locales/simple_form.nl.yml +++ b/config/locales/simple_form.nl.yml @@ -61,6 +61,7 @@ nl: setting_display_media_default: Als gevoelig gemarkeerde media verbergen setting_display_media_hide_all: Media altijd verbergen setting_display_media_show_all: Media altijd tonen + setting_emoji_style: Waarmee moeten emojis worden weergegeven. "Auto" probeert de systeemeigen emojis te gebruiken, maar valt terug op Twemoji voor oudere webbrowsers. setting_system_scrollbars_ui: Alleen van toepassing op desktopbrowsers gebaseerd op Safari en Chrome setting_use_blurhash: Wazige kleurovergangen zijn gebaseerd op de kleuren van de verborgen media, waarmee elk detail verdwijnt setting_use_pending_items: De tijdlijn wordt bijgewerkt door op het aantal nieuwe items te klikken, in plaats van dat deze automatisch wordt bijgewerkt @@ -241,6 +242,7 @@ nl: setting_display_media_default: Standaard setting_display_media_hide_all: Alles verbergen setting_display_media_show_all: Alles tonen + setting_emoji_style: Emoji-stijl setting_expand_spoilers: Berichten met inhoudswaarschuwingen altijd uitklappen setting_hide_network: Jouw volgers en wie je volgt verbergen setting_missing_alt_text_modal: Bevestigingsvenster tonen voor het plaatsen van media zonder alt-tekst diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml index 80bc56d033..e520541273 100644 --- a/config/locales/simple_form.tr.yml +++ b/config/locales/simple_form.tr.yml @@ -61,6 +61,7 @@ tr: setting_display_media_default: Hassas olarak işaretlenmiş medyayı gizle setting_display_media_hide_all: Medyayı her zaman gizle setting_display_media_show_all: Medyayı her zaman göster + setting_emoji_style: Emojiler nasıl görüntülensin. "Otomatik" seçeneği yerel emojileri kullanmaya çalışır, ancak eski tarayıcılar için Twemoji'yi kullanır. setting_system_scrollbars_ui: Yalnızca Safari ve Chrome tabanlı masaüstü tarayıcılar için geçerlidir setting_use_blurhash: Gradyenler gizli görsellerin renklerine dayanır, ancak detayları gizler setting_use_pending_items: Akışı otomatik olarak kaydırmak yerine, zaman çizelgesi güncellemelerini tek bir tıklamayla gizleyin @@ -241,6 +242,7 @@ tr: setting_display_media_default: Varsayılan setting_display_media_hide_all: Tümünü gizle setting_display_media_show_all: Tümünü göster + setting_emoji_style: Emoji stili setting_expand_spoilers: İçerik uyarılarıyla işaretli gönderileri her zaman genişlet setting_hide_network: Sosyal grafiğini gizle setting_missing_alt_text_modal: Alternatif metni olmayan medya göndermeden önce onay sorusu göster diff --git a/config/locales/simple_form.vi.yml b/config/locales/simple_form.vi.yml index 9e794028c5..556bcb2e90 100644 --- a/config/locales/simple_form.vi.yml +++ b/config/locales/simple_form.vi.yml @@ -61,6 +61,7 @@ vi: setting_display_media_default: Click để xem setting_display_media_hide_all: Luôn ẩn setting_display_media_show_all: Luôn hiện + setting_emoji_style: Cách hiển thị Emoji. "Tự động" sẽ dùng biểu tượng cảm xúc nguyên bản, nhưng đối với các trình duyệt cũ sẽ chuyển thành Twemoji. setting_system_scrollbars_ui: Chỉ áp dụng trình duyệt Chrome và Safari bản desktop setting_use_blurhash: Phủ lớp màu làm nhòe đi hình ảnh nhạy cảm setting_use_pending_items: Dồn lại toàn bộ tút mới và chỉ hiển thị khi nhấn vào @@ -240,6 +241,7 @@ vi: setting_display_media_default: Mặc định setting_display_media_hide_all: Ẩn toàn bộ setting_display_media_show_all: Hiện toàn bộ + setting_emoji_style: Phong cách Emoji setting_expand_spoilers: Luôn mở rộng tút chứa nội dung ẩn setting_hide_network: Ẩn quan hệ của bạn setting_missing_alt_text_modal: Hỏi trước khi đăng media không có văn bản thay thế diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index 7716c2232f..3c9fc97471 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -240,6 +240,7 @@ zh-CN: setting_display_media_default: 默认 setting_display_media_hide_all: 隐藏全部 setting_display_media_show_all: 显示全部 + setting_emoji_style: 表情符号样式 setting_expand_spoilers: 一律展开具有内容警告的嘟文 setting_hide_network: 隐藏你的社交网络 setting_missing_alt_text_modal: 发布媒体时若未为其设置替代文本,则显示确认对话框 diff --git a/config/locales/simple_form.zh-TW.yml b/config/locales/simple_form.zh-TW.yml index fb51f2ed3e..83feb30eba 100644 --- a/config/locales/simple_form.zh-TW.yml +++ b/config/locales/simple_form.zh-TW.yml @@ -61,6 +61,7 @@ zh-TW: setting_display_media_default: 隱藏標為敏感內容的媒體 setting_display_media_hide_all: 總是隱藏所有媒體 setting_display_media_show_all: 總是顯示標為敏感內容的媒體 + setting_emoji_style: 如何顯示 emoji 表情符號。「自動」將嘗試使用原生 emoji ,但於老式瀏覽器使用 Twemoji。 setting_system_scrollbars_ui: 僅套用至基於 Safari 或 Chrome 之桌面瀏覽器 setting_use_blurhash: 彩色漸層圖樣是基於隱藏媒體內容顏色產生,所有細節將變得模糊 setting_use_pending_items: 關閉自動捲動更新,時間軸僅於點擊後更新 @@ -240,6 +241,7 @@ zh-TW: setting_display_media_default: 預設 setting_display_media_hide_all: 全部隱藏 setting_display_media_show_all: 全部顯示 + setting_emoji_style: emoji 風格 setting_expand_spoilers: 永遠展開標有內容警告的嘟文 setting_hide_network: 隱藏您的社交網路 setting_missing_alt_text_modal: 發表未包含說明文字之多媒體嘟文前先詢問我 diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 7e681c9031..89c51ca6e0 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -1349,6 +1349,10 @@ tr: basic_information: Temel bilgiler hint_html: "İnsanlara herkese açık profilinizde ve gönderilerinizin yanında ne göstermek istediğinizi düzenleyin. Dolu bir profile ve bir profil resmine sahip olduğunuzda diğer insanlar daha yüksek ihtimalle sizi takip etmek ve sizinle etkileşime geçmek isteyeceklerdir." other: Diğer + emoji_styles: + auto: Otomatik + native: Yerel + twemoji: Twemoji errors: '400': Gönderdiğiniz istek geçersiz veya hatalı biçimlendirilmiş. '403': Bu sayfayı görmek için izniniz yok. diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 93ab1fb811..b8e2b9ef72 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -1330,6 +1330,10 @@ vi: basic_information: Thông tin cơ bản hint_html: Mọi người sẽ muốn theo dõi và tương tác với bạn hơn nếu bạn có ảnh đại diện và hồ sơ hoàn chỉnh. other: Khác + emoji_styles: + auto: Tự động + native: Nguyên bản + twemoji: Twemoji errors: '400': Yêu cầu bạn gửi không hợp lệ hoặc sai hình thức. '403': Bạn không có quyền xem trang này. diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index ddd2c68113..c62dc8e319 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -1321,6 +1321,10 @@ zh-CN: basic_information: 基本信息 hint_html: "自定义公开资料和嘟文旁边显示的内容。当你填写完整的个人资料并设置了头像时,其他人更有可能关注你并与你互动。" other: 其他 + emoji_styles: + auto: 自动 + native: 原生 + twemoji: Twemoji errors: '400': 你提交的请求无效或格式不正确。 '403': 你没有访问此页面的权限。 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index 7c895d6794..546ac7d989 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1332,6 +1332,10 @@ zh-TW: basic_information: 基本資訊 hint_html: "自訂人們能於您個人檔案及嘟文旁所見之內容。當您完成填寫個人檔案及設定大頭貼後,其他人們比較願意跟隨您並與您互動。" other: 其他 + emoji_styles: + auto: 自動 + native: 原生風格 + twemoji: Twemoji errors: '400': 您所送出的請求無效或格式不正確。 '403': 您沒有檢視這個頁面的權限。 From a1e88135225753544197d8f1dd57fbb58cead43f Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 9 Jul 2025 11:55:41 +0200 Subject: [PATCH 06/10] Emoji Indexing and Search (#35253) --- .../mastodon/features/emoji/constants.ts | 110 ++++++++++++++ .../mastodon/features/emoji/database.ts | 102 +++++++++++++ .../mastodon/features/emoji/index.ts | 38 +++++ .../mastodon/features/emoji/loader.ts | 77 ++++++++++ .../mastodon/features/emoji/locale.test.ts | 61 ++------ .../mastodon/features/emoji/locale.ts | 56 ++------ .../mastodon/features/emoji/normalize.test.ts | 135 +++++++++++------- .../mastodon/features/emoji/normalize.ts | 78 +++++++--- .../mastodon/features/emoji/worker.ts | 13 ++ package.json | 2 + vite.config.mts | 25 +++- yarn.lock | 106 +++++++++++++- 12 files changed, 632 insertions(+), 171 deletions(-) create mode 100644 app/javascript/mastodon/features/emoji/constants.ts create mode 100644 app/javascript/mastodon/features/emoji/database.ts create mode 100644 app/javascript/mastodon/features/emoji/index.ts create mode 100644 app/javascript/mastodon/features/emoji/loader.ts create mode 100644 app/javascript/mastodon/features/emoji/worker.ts diff --git a/app/javascript/mastodon/features/emoji/constants.ts b/app/javascript/mastodon/features/emoji/constants.ts new file mode 100644 index 0000000000..d38f17f216 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/constants.ts @@ -0,0 +1,110 @@ +// Utility codes +export const VARIATION_SELECTOR_CODE = 0xfe0f; +export const KEYCAP_CODE = 0x20e3; + +// Gender codes +export const GENDER_FEMALE_CODE = 0x2640; +export const GENDER_MALE_CODE = 0x2642; + +// Skin tone codes +export const SKIN_TONE_CODES = [ + 0x1f3fb, // Light skin tone + 0x1f3fc, // Medium-light skin tone + 0x1f3fd, // Medium skin tone + 0x1f3fe, // Medium-dark skin tone + 0x1f3ff, // Dark skin tone +] as const; + +export const EMOJIS_WITH_DARK_BORDER = [ + '🎱', // 1F3B1 + '🐜', // 1F41C + '⚫', // 26AB + '🖤', // 1F5A4 + '⬛', // 2B1B + '◼️', // 25FC-FE0F + '◾', // 25FE + '◼️', // 25FC-FE0F + '✒️', // 2712-FE0F + '▪️', // 25AA-FE0F + '💣', // 1F4A3 + '🎳', // 1F3B3 + '📷', // 1F4F7 + '📸', // 1F4F8 + '♣️', // 2663-FE0F + '🕶️', // 1F576-FE0F + '✴️', // 2734-FE0F + '🔌', // 1F50C + '💂‍♀️', // 1F482-200D-2640-FE0F + '📽️', // 1F4FD-FE0F + '🍳', // 1F373 + '🦍', // 1F98D + '💂', // 1F482 + '🔪', // 1F52A + '🕳️', // 1F573-FE0F + '🕹️', // 1F579-FE0F + '🕋', // 1F54B + '🖊️', // 1F58A-FE0F + '🖋️', // 1F58B-FE0F + '💂‍♂️', // 1F482-200D-2642-FE0F + '🎤', // 1F3A4 + '🎓', // 1F393 + '🎥', // 1F3A5 + '🎼', // 1F3BC + '♠️', // 2660-FE0F + '🎩', // 1F3A9 + '🦃', // 1F983 + '📼', // 1F4FC + '📹', // 1F4F9 + '🎮', // 1F3AE + '🐃', // 1F403 + '🏴', // 1F3F4 + '🐞', // 1F41E + '🕺', // 1F57A + '📱', // 1F4F1 + '📲', // 1F4F2 + '🚲', // 1F6B2 + '🪮', // 1FAA6 + '🐦‍⬛', // 1F426-200D-2B1B +]; + +export const EMOJIS_WITH_LIGHT_BORDER = [ + '👽', // 1F47D + '⚾', // 26BE + '🐔', // 1F414 + '☁️', // 2601-FE0F + '💨', // 1F4A8 + '🕊️', // 1F54A-FE0F + '👀', // 1F440 + '🍥', // 1F365 + '👻', // 1F47B + '🐐', // 1F410 + '❕', // 2755 + '❔', // 2754 + '⛸️', // 26F8-FE0F + '🌩️', // 1F329-FE0F + '🔊', // 1F50A + '🔇', // 1F507 + '📃', // 1F4C3 + '🌧️', // 1F327-FE0F + '🐏', // 1F40F + '🍚', // 1F35A + '🍙', // 1F359 + '🐓', // 1F413 + '🐑', // 1F411 + '💀', // 1F480 + '☠️', // 2620-FE0F + '🌨️', // 1F328-FE0F + '🔉', // 1F509 + '🔈', // 1F508 + '💬', // 1F4AC + '💭', // 1F4AD + '🏐', // 1F3D0 + '🏳️', // 1F3F3-FE0F + '⚪', // 26AA + '⬜', // 2B1C + '◽', // 25FD + '◻️', // 25FB-FE0F + '▫️', // 25AB-FE0F + '🪽', // 1FAE8 + '🪿', // 1FABF +]; diff --git a/app/javascript/mastodon/features/emoji/database.ts b/app/javascript/mastodon/features/emoji/database.ts new file mode 100644 index 0000000000..618f010850 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/database.ts @@ -0,0 +1,102 @@ +import { SUPPORTED_LOCALES } from 'emojibase'; +import type { FlatCompactEmoji, Locale } from 'emojibase'; +import type { DBSchema } from 'idb'; +import { openDB } from 'idb'; + +import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; + +import type { LocaleOrCustom } from './locale'; +import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; + +interface EmojiDB extends LocaleTables, DBSchema { + custom: { + key: string; + value: ApiCustomEmojiJSON; + indexes: { + category: string; + }; + }; + etags: { + key: LocaleOrCustom; + value: string; + }; +} + +interface LocaleTable { + key: string; + value: FlatCompactEmoji; + indexes: { + group: number; + label: string; + order: number; + tags: string[]; + }; +} +type LocaleTables = Record; + +const SCHEMA_VERSION = 1; + +const db = await openDB('mastodon-emoji', SCHEMA_VERSION, { + upgrade(database) { + const customTable = database.createObjectStore('custom', { + keyPath: 'shortcode', + autoIncrement: false, + }); + customTable.createIndex('category', 'category'); + + database.createObjectStore('etags'); + + for (const locale of SUPPORTED_LOCALES) { + const localeTable = database.createObjectStore(locale, { + keyPath: 'hexcode', + autoIncrement: false, + }); + localeTable.createIndex('group', 'group'); + localeTable.createIndex('label', 'label'); + localeTable.createIndex('order', 'order'); + localeTable.createIndex('tags', 'tags', { multiEntry: true }); + } + }, +}); + +export async function putEmojiData(emojis: FlatCompactEmoji[], locale: Locale) { + const trx = db.transaction(locale, 'readwrite'); + await Promise.all(emojis.map((emoji) => trx.store.put(emoji))); + await trx.done; +} + +export async function putCustomEmojiData(emojis: ApiCustomEmojiJSON[]) { + const trx = db.transaction('custom', 'readwrite'); + await Promise.all(emojis.map((emoji) => trx.store.put(emoji))); + await trx.done; +} + +export function putLatestEtag(etag: string, localeString: string) { + const locale = toSupportedLocaleOrCustom(localeString); + return db.put('etags', etag, locale); +} + +export function searchEmojiByHexcode(hexcode: string, localeString: string) { + const locale = toSupportedLocale(localeString); + return db.get(locale, hexcode); +} + +export function searchEmojiByTag(tag: string, localeString: string) { + const locale = toSupportedLocale(localeString); + const range = IDBKeyRange.only(tag.toLowerCase()); + return db.getAllFromIndex(locale, 'tags', range); +} + +export function searchCustomEmojiByShortcode(shortcode: string) { + return db.get('custom', shortcode); +} + +export async function loadLatestEtag(localeString: string) { + const locale = toSupportedLocaleOrCustom(localeString); + const rowCount = await db.count(locale); + if (!rowCount) { + return null; // No data for this locale, return null even if there is an etag. + } + const etag = await db.get('etags', locale); + return etag ?? null; +} diff --git a/app/javascript/mastodon/features/emoji/index.ts b/app/javascript/mastodon/features/emoji/index.ts new file mode 100644 index 0000000000..6975465b55 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/index.ts @@ -0,0 +1,38 @@ +import initialState from '@/mastodon/initial_state'; + +import { toSupportedLocale } from './locale'; + +const serverLocale = toSupportedLocale(initialState?.meta.locale ?? 'en'); + +const worker = + 'Worker' in window + ? new Worker(new URL('./worker', import.meta.url), { + type: 'module', + }) + : null; + +export async function initializeEmoji() { + if (worker) { + worker.addEventListener('message', (event: MessageEvent) => { + const { data: message } = event; + if (message === 'ready') { + worker.postMessage(serverLocale); + worker.postMessage('custom'); + } + }); + } else { + const { importCustomEmojiData, importEmojiData } = await import('./loader'); + await Promise.all([importCustomEmojiData(), importEmojiData(serverLocale)]); + } +} + +export async function loadEmojiLocale(localeString: string) { + const locale = toSupportedLocale(localeString); + + if (worker) { + worker.postMessage(locale); + } else { + const { importEmojiData } = await import('./loader'); + await importEmojiData(locale); + } +} diff --git a/app/javascript/mastodon/features/emoji/loader.ts b/app/javascript/mastodon/features/emoji/loader.ts new file mode 100644 index 0000000000..f9c6971351 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/loader.ts @@ -0,0 +1,77 @@ +import { flattenEmojiData } from 'emojibase'; +import type { CompactEmoji, FlatCompactEmoji } from 'emojibase'; + +import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; +import { isDevelopment } from '@/mastodon/utils/environment'; + +import { + putEmojiData, + putCustomEmojiData, + loadLatestEtag, + putLatestEtag, +} from './database'; +import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; +import type { LocaleOrCustom } from './locale'; + +export async function importEmojiData(localeString: string) { + const locale = toSupportedLocale(localeString); + const emojis = await fetchAndCheckEtag(locale); + if (!emojis) { + return; + } + const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis); + await putEmojiData(flattenedEmojis, locale); +} + +export async function importCustomEmojiData() { + const emojis = await fetchAndCheckEtag('custom'); + if (!emojis) { + return; + } + await putCustomEmojiData(emojis); +} + +async function fetchAndCheckEtag( + localeOrCustom: LocaleOrCustom, +): Promise { + const locale = toSupportedLocaleOrCustom(localeOrCustom); + + let uri: string; + if (locale === 'custom') { + uri = '/api/v1/custom_emojis'; + } else { + uri = `/packs${isDevelopment() ? '-dev' : ''}/emoji/${locale}.json`; + } + + const oldEtag = await loadLatestEtag(locale); + const response = await fetch(uri, { + headers: { + 'Content-Type': 'application/json', + 'If-None-Match': oldEtag ?? '', // Send the old ETag to check for modifications + }, + }); + // If not modified, return null + if (response.status === 304) { + return null; + } + if (!response.ok) { + throw new Error( + `Failed to fetch emoji data for ${localeOrCustom}: ${response.statusText}`, + ); + } + + const data = (await response.json()) as ResultType; + if (!Array.isArray(data)) { + throw new Error( + `Unexpected data format for ${localeOrCustom}: expected an array`, + ); + } + + // Store the ETag for future requests + const etag = response.headers.get('ETag'); + if (etag) { + await putLatestEtag(etag, localeOrCustom); + } + + return data; +} diff --git a/app/javascript/mastodon/features/emoji/locale.test.ts b/app/javascript/mastodon/features/emoji/locale.test.ts index 0e098b2d44..5a474e9428 100644 --- a/app/javascript/mastodon/features/emoji/locale.test.ts +++ b/app/javascript/mastodon/features/emoji/locale.test.ts @@ -1,52 +1,6 @@ -import { flattenEmojiData, SUPPORTED_LOCALES } from 'emojibase'; -import emojiEnData from 'emojibase-data/en/compact.json'; -import emojiFrData from 'emojibase-data/fr/compact.json'; +import { SUPPORTED_LOCALES } from 'emojibase'; -import { toSupportedLocale, unicodeToLocaleLabel } from './locale'; - -describe('unicodeToLocaleLabel', () => { - const emojiTestCases = [ - '1F3CB-1F3FF-200D-2640-FE0F', // 🏋🏿‍♀️ Woman weightlifter, dark skin - '1F468-1F3FB', // 👨🏻 Man, light skin - '1F469-1F3FB-200D-2695-FE0F', // 👩🏻‍⚕️ Woman health worker, light skin - '1F468-1F3FD-200D-1F692', // 👨🏽‍🚒 Man firefighter, medium skin - '1F469-1F3FE', // 👩🏾 Woman, medium-dark skin - '1F469-1F3FF-200D-1F4BB', // 👩🏿‍💻 Woman technologist, dark skin - '1F478-1F3FF', // 👸🏿 Princess with dark skin tone - '1F935-1F3FC-200D-2640-FE0F', // 🤵🏼‍♀️ Woman in tuxedo, medium-light skin - '1F9D1-1F3FC', // 🧑🏼 Person, medium-light skin - '1F9D4-1F3FE', // 🧔🏾 Person with beard, medium-dark skin - ]; - - const flattenedEnData = flattenEmojiData(emojiEnData); - const flattenedFrData = flattenEmojiData(emojiFrData); - - const emojiTestEnLabels = new Map( - emojiTestCases.map((code) => [ - code, - flattenedEnData.find((emoji) => emoji.hexcode === code)?.label, - ]), - ); - const emojiTestFrLabels = new Map( - emojiTestCases.map((code) => [ - code, - flattenedFrData.find((emoji) => emoji.hexcode === code)?.label, - ]), - ); - - test.for( - emojiTestCases.flatMap((code) => [ - [code, 'en', emojiTestEnLabels.get(code)], - [code, 'fr', emojiTestFrLabels.get(code)], - ]) satisfies [string, string, string | undefined][], - )( - 'returns correct label for %s for %s locale', - async ([unicodeHex, locale, expectedLabel]) => { - const label = await unicodeToLocaleLabel(unicodeHex, locale); - expect(label).toBe(expectedLabel); - }, - ); -}); +import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; describe('toSupportedLocale', () => { test('returns the same locale if it is supported', () => { @@ -62,3 +16,14 @@ describe('toSupportedLocale', () => { } }); }); + +describe('toSupportedLocaleOrCustom', () => { + test('returns custom for "custom" locale', () => { + expect(toSupportedLocaleOrCustom('custom')).toBe('custom'); + }); + test('returns supported locale for valid locales', () => { + for (const locale of SUPPORTED_LOCALES) { + expect(toSupportedLocaleOrCustom(locale)).toBe(locale); + } + }); +}); diff --git a/app/javascript/mastodon/features/emoji/locale.ts b/app/javascript/mastodon/features/emoji/locale.ts index aac6c376b0..561c94afb0 100644 --- a/app/javascript/mastodon/features/emoji/locale.ts +++ b/app/javascript/mastodon/features/emoji/locale.ts @@ -1,51 +1,23 @@ -import type { CompactEmoji, Locale } from 'emojibase'; -import { flattenEmojiData, SUPPORTED_LOCALES } from 'emojibase'; +import type { Locale } from 'emojibase'; +import { SUPPORTED_LOCALES } from 'emojibase'; -// Simple cache. This will be replaced with an IndexedDB cache in the future. -const localeCache = new Map>(); +export type LocaleOrCustom = Locale | 'custom'; -export async function unicodeToLocaleLabel( - unicodeHex: string, - localeString: string, -) { - const locale = toSupportedLocale(localeString); - let hexMap = localeCache.get(locale); - if (!hexMap) { - hexMap = await loadLocaleLabels(locale); - localeCache.set(locale, hexMap); - } - - const label = hexMap.get(unicodeHex)?.label; - if (!label) { - throw new Error( - `Label for unicode hex ${unicodeHex} not found in locale ${locale}`, - ); - } - return label; -} - -async function loadLocaleLabels( - locale: Locale, -): Promise> { - const { default: localeEmoji } = ((await import( - `emojibase-data/${locale}/compact.json` - )) ?? { default: [] }) as { default: CompactEmoji[] }; - if (!Array.isArray(localeEmoji)) { - throw new Error(`Locale data for ${locale} not found`); - } - const hexMapEntries = flattenEmojiData(localeEmoji).map( - (emoji) => [emoji.hexcode, emoji] satisfies [string, CompactEmoji], - ); - return new Map(hexMapEntries); -} - -export function toSupportedLocale(locale: string): Locale { +export function toSupportedLocale(localeBase: string): Locale { + const locale = localeBase.toLowerCase(); if (isSupportedLocale(locale)) { return locale; } return 'en'; // Default to English if unsupported } -function isSupportedLocale(locale: string): locale is Locale { - return SUPPORTED_LOCALES.includes(locale as Locale); +export function toSupportedLocaleOrCustom(locale: string): LocaleOrCustom { + if (locale.toLowerCase() === 'custom') { + return 'custom'; + } + return toSupportedLocale(locale); +} + +function isSupportedLocale(locale: string): locale is Locale { + return SUPPORTED_LOCALES.includes(locale.toLowerCase() as Locale); } diff --git a/app/javascript/mastodon/features/emoji/normalize.test.ts b/app/javascript/mastodon/features/emoji/normalize.test.ts index 29255d5291..ee9cd89487 100644 --- a/app/javascript/mastodon/features/emoji/normalize.test.ts +++ b/app/javascript/mastodon/features/emoji/normalize.test.ts @@ -1,9 +1,17 @@ import { readdir } from 'fs/promises'; import { basename, resolve } from 'path'; -import unicodeEmojis from 'emojibase-data/en/data.json'; +import { flattenEmojiData } from 'emojibase'; +import unicodeRawEmojis from 'emojibase-data/en/data.json'; -import { twemojiToUnicodeInfo, unicodeToTwemojiHex } from './normalize'; +import { + twemojiHasBorder, + twemojiToUnicodeInfo, + unicodeToTwemojiHex, + CODES_WITH_DARK_BORDER, + CODES_WITH_LIGHT_BORDER, + emojiToUnicodeHex, +} from './normalize'; const emojiSVGFiles = await readdir( // This assumes tests are run from project root @@ -13,60 +21,81 @@ const emojiSVGFiles = await readdir( }, ); const svgFileNames = emojiSVGFiles - .filter( - (file) => - file.isFile() && - file.name.endsWith('.svg') && - !file.name.endsWith('_border.svg'), - ) + .filter((file) => file.isFile() && file.name.endsWith('.svg')) .map((file) => basename(file.name, '.svg').toUpperCase()); +const svgFileNamesWithoutBorder = svgFileNames.filter( + (fileName) => !fileName.endsWith('_BORDER'), +); -describe('normalizeEmoji', () => { - describe('unicodeToSVGName', () => { - test.concurrent.for( - unicodeEmojis - // Our version of Twemoji only supports up to version 15.1 - .filter((emoji) => emoji.version < 16) - .map((emoji) => [emoji.hexcode, emoji.label] as [string, string]), - )('verifying an emoji exists for %s (%s)', ([hexcode], { expect }) => { - const result = unicodeToTwemojiHex(hexcode); - expect(svgFileNames).toContain(result); - }); - }); +const unicodeEmojis = flattenEmojiData(unicodeRawEmojis); - describe('twemojiToUnicodeInfo', () => { - const unicodeMap = new Map( - unicodeEmojis.flatMap((emoji) => { - const base: [string, string][] = [[emoji.hexcode, emoji.label]]; - if (emoji.skins) { - base.push( - ...emoji.skins.map( - (skin) => [skin.hexcode, skin.label] as [string, string], - ), - ); - } - return base; - }), - ); +describe('emojiToUnicodeHex', () => { + test.concurrent.for([ + ['🎱', '1F3B1'], + ['🐜', '1F41C'], + ['⚫', '26AB'], + ['🖤', '1F5A4'], + ['💀', '1F480'], + ['💂‍♂️', '1F482-200D-2642-FE0F'], + ] as const)( + 'emojiToUnicodeHex converts %s to %s', + ([emoji, hexcode], { expect }) => { + expect(emojiToUnicodeHex(emoji)).toBe(hexcode); + }, + ); +}); - test.concurrent.for(svgFileNames)( - 'verifying SVG file %s maps to Unicode emoji', - (svgFileName, { expect }) => { - assert(!!svgFileName); - const result = twemojiToUnicodeInfo(svgFileName); - const hexcode = - typeof result === 'string' ? result : result.unqualified; - if (!hexcode) { - // No hexcode means this is a special case like the Shibuya 109 emoji - expect(result).toHaveProperty('label'); - return; - } - assert(!!hexcode); - expect( - unicodeMap.has(hexcode), - `${hexcode} (${svgFileName}) not found`, - ).toBeTruthy(); - }, - ); +describe('unicodeToTwemojiHex', () => { + test.concurrent.for( + unicodeEmojis + // Our version of Twemoji only supports up to version 15.1 + .filter((emoji) => emoji.version < 16) + .map((emoji) => [emoji.hexcode, emoji.label] as [string, string]), + )('verifying an emoji exists for %s (%s)', ([hexcode], { expect }) => { + const result = unicodeToTwemojiHex(hexcode); + expect(svgFileNamesWithoutBorder).toContain(result); }); }); + +describe('twemojiHasBorder', () => { + test.concurrent.for( + svgFileNames + .filter((file) => file.endsWith('_BORDER')) + .map((file) => { + const hexCode = file.replace('_BORDER', ''); + return [ + hexCode, + CODES_WITH_LIGHT_BORDER.includes(hexCode), + CODES_WITH_DARK_BORDER.includes(hexCode), + ] as const; + }), + )('twemojiHasBorder for %s', ([hexCode, isLight, isDark], { expect }) => { + const result = twemojiHasBorder(hexCode); + expect(result).toHaveProperty('hexCode', hexCode); + expect(result).toHaveProperty('hasLightBorder', isLight); + expect(result).toHaveProperty('hasDarkBorder', isDark); + }); +}); + +describe('twemojiToUnicodeInfo', () => { + const unicodeCodeSet = new Set(unicodeEmojis.map((emoji) => emoji.hexcode)); + + test.concurrent.for(svgFileNamesWithoutBorder)( + 'verifying SVG file %s maps to Unicode emoji', + (svgFileName, { expect }) => { + assert(!!svgFileName); + const result = twemojiToUnicodeInfo(svgFileName); + const hexcode = typeof result === 'string' ? result : result.unqualified; + if (!hexcode) { + // No hexcode means this is a special case like the Shibuya 109 emoji + expect(result).toHaveProperty('label'); + return; + } + assert(!!hexcode); + expect( + unicodeCodeSet.has(hexcode), + `${hexcode} (${svgFileName}) not found`, + ).toBeTruthy(); + }, + ); +}); diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index 024cd53625..94dc33a6ea 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -1,19 +1,12 @@ -// Utility codes -const VARIATION_SELECTOR_CODE = 0xfe0f; -const KEYCAP_CODE = 0x20e3; - -// Gender codes -const GENDER_FEMALE_CODE = 0x2640; -const GENDER_MALE_CODE = 0x2642; - -// Skin tone codes -const SKIN_TONE_CODES = [ - 0x1f3fb, // Light skin tone - 0x1f3fc, // Medium-light skin tone - 0x1f3fd, // Medium skin tone - 0x1f3fe, // Medium-dark skin tone - 0x1f3ff, // Dark skin tone -] as const; +import { + VARIATION_SELECTOR_CODE, + KEYCAP_CODE, + GENDER_FEMALE_CODE, + GENDER_MALE_CODE, + SKIN_TONE_CODES, + EMOJIS_WITH_DARK_BORDER, + EMOJIS_WITH_LIGHT_BORDER, +} from './constants'; // Misc codes that have special handling const SKIER_CODE = 0x26f7; @@ -24,6 +17,17 @@ const LEVITATING_PERSON_CODE = 0x1f574; const SPEECH_BUBBLE_CODE = 0x1f5e8; const MS_CLAUS_CODE = 0x1f936; +export function emojiToUnicodeHex(emoji: string): string { + const codes: number[] = []; + for (const char of emoji) { + const code = char.codePointAt(0); + if (code !== undefined) { + codes.push(code); + } + } + return hexNumbersToString(codes); +} + export function unicodeToTwemojiHex(unicodeHex: string): string { const codes = hexStringToNumbers(unicodeHex); const normalizedCodes: number[] = []; @@ -50,6 +54,35 @@ export function unicodeToTwemojiHex(unicodeHex: string): string { return hexNumbersToString(normalizedCodes, 0); } +interface TwemojiBorderInfo { + hexCode: string; + hasLightBorder: boolean; + hasDarkBorder: boolean; +} + +export const CODES_WITH_DARK_BORDER = + EMOJIS_WITH_DARK_BORDER.map(emojiToUnicodeHex); + +export const CODES_WITH_LIGHT_BORDER = + EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex); + +export function twemojiHasBorder(twemojiHex: string): TwemojiBorderInfo { + const normalizedHex = twemojiHex.toUpperCase(); + let hasLightBorder = false; + let hasDarkBorder = false; + if (CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) { + hasLightBorder = true; + } + if (CODES_WITH_DARK_BORDER.includes(normalizedHex)) { + hasDarkBorder = true; + } + return { + hexCode: normalizedHex, + hasLightBorder, + hasDarkBorder, + }; +} + interface TwemojiSpecificEmoji { unqualified?: string; gender?: number; @@ -84,11 +117,16 @@ export function twemojiToUnicodeInfo( let gender: undefined | number; let skin: undefined | number; for (const code of codes) { - if (code in GENDER_CODES_MAP) { + if (!gender && code in GENDER_CODES_MAP) { gender = GENDER_CODES_MAP[code]; - } else if (code in SKIN_TONE_CODES) { + } else if (!skin && code in SKIN_TONE_CODES) { skin = code; } + + // Exit if we have both skin and gender + if (skin && gender) { + break; + } } let mappedCodes: unknown[] = codes; @@ -103,8 +141,8 @@ export function twemojiToUnicodeInfo( // For key emoji, insert the variation selector mappedCodes = [codes[0], VARIATION_SELECTOR_CODE, KEYCAP_CODE]; } else if ( - codes.at(0) === SKIER_CODE || - codes.at(0) === LEVITATING_PERSON_CODE + (codes.at(0) === SKIER_CODE || codes.at(0) === LEVITATING_PERSON_CODE) && + codes.length > 1 ) { // Twemoji offers more gender and skin options for the skier and levitating person emoji. return { diff --git a/app/javascript/mastodon/features/emoji/worker.ts b/app/javascript/mastodon/features/emoji/worker.ts new file mode 100644 index 0000000000..1c48a07773 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/worker.ts @@ -0,0 +1,13 @@ +import { importEmojiData, importCustomEmojiData } from './loader'; + +addEventListener('message', handleMessage); +self.postMessage('ready'); // After the worker is ready, notify the main thread + +function handleMessage(event: MessageEvent) { + const { data: locale } = event; + if (locale !== 'custom') { + void importEmojiData(locale); + } else { + void importCustomEmojiData(); + } +} diff --git a/package.json b/package.json index 2670422334..ace593ab01 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "history": "^4.10.1", "hoist-non-react-statics": "^3.3.2", "http-link-header": "^1.1.1", + "idb": "^8.0.3", "immutable": "^4.3.0", "intl-messageformat": "^10.7.16", "js-yaml": "^4.1.0", @@ -117,6 +118,7 @@ "vite-plugin-pwa": "^1.0.0", "vite-plugin-rails": "^0.5.0", "vite-plugin-ruby": "^5.1.1", + "vite-plugin-static-copy": "^3.1.0", "vite-plugin-svgr": "^4.3.0", "vite-tsconfig-paths": "^5.1.4", "wicg-inert": "^3.1.2", diff --git a/vite.config.mts b/vite.config.mts index 484211eaa0..6429f97c59 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -1,14 +1,15 @@ import path from 'node:path'; import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin'; +import legacy from '@vitejs/plugin-legacy'; import react from '@vitejs/plugin-react'; import { PluginOption } from 'vite'; -import svgr from 'vite-plugin-svgr'; import { visualizer } from 'rollup-plugin-visualizer'; -import RailsPlugin from 'vite-plugin-rails'; import { VitePWA } from 'vite-plugin-pwa'; +import RailsPlugin from 'vite-plugin-rails'; +import { viteStaticCopy } from 'vite-plugin-static-copy'; +import svgr from 'vite-plugin-svgr'; import tsconfigPaths from 'vite-tsconfig-paths'; -import legacy from '@vitejs/plugin-legacy'; import { defineConfig, UserConfigFnPromise, UserConfig } from 'vite'; import postcssPresetEnv from 'postcss-preset-env'; @@ -78,6 +79,9 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { }, }, }, + worker: { + format: 'es', + }, plugins: [ tsconfigPaths(), RailsPlugin({ @@ -92,6 +96,21 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { plugins: ['formatjs', 'transform-react-remove-prop-types'], }, }), + viteStaticCopy({ + targets: [ + { + src: path.resolve( + __dirname, + 'node_modules/emojibase-data/**/compact.json', + ), + dest: 'emoji', + rename(_name, ext, dir) { + const locale = path.basename(path.dirname(dir)); + return `${locale}.${ext}`; + }, + }, + ], + }), MastodonServiceWorkerLocales(), MastodonEmojiCompressed(), legacy({ diff --git a/yarn.lock b/yarn.lock index a1aea84b5e..a0814a27af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2686,6 +2686,7 @@ __metadata: hoist-non-react-statics: "npm:^3.3.2" http-link-header: "npm:^1.1.1" husky: "npm:^9.0.11" + idb: "npm:^8.0.3" immutable: "npm:^4.3.0" intl-messageformat: "npm:^10.7.16" js-yaml: "npm:^4.1.0" @@ -2742,6 +2743,7 @@ __metadata: vite-plugin-pwa: "npm:^1.0.0" vite-plugin-rails: "npm:^0.5.0" vite-plugin-ruby: "npm:^5.1.1" + vite-plugin-static-copy: "npm:^3.1.0" vite-plugin-svgr: "npm:^4.2.0" vite-tsconfig-paths: "npm:^5.1.4" vitest: "npm:^3.2.1" @@ -5083,6 +5085,16 @@ __metadata: languageName: node linkType: hard +"anymatch@npm:~3.1.2": + version: 3.1.3 + resolution: "anymatch@npm:3.1.3" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10c0/57b06ae984bc32a0d22592c87384cd88fe4511b1dd7581497831c56d41939c8a001b28e7b853e1450f2bf61992dfcaa8ae2d0d161a0a90c4fb631ef07098fbac + languageName: node + linkType: hard + "are-docs-informative@npm:^0.0.2": version: 0.0.2 resolution: "are-docs-informative@npm:0.0.2" @@ -5471,6 +5483,13 @@ __metadata: languageName: node linkType: hard +"binary-extensions@npm:^2.0.0": + version: 2.3.0 + resolution: "binary-extensions@npm:2.3.0" + checksum: 10c0/75a59cafc10fb12a11d510e77110c6c7ae3f4ca22463d52487709ca7f18f69d886aa387557cc9864fbdb10153d0bdb4caacabf11541f55e89ed6e18d12ece2b5 + languageName: node + linkType: hard + "bintrees@npm:1.0.2": version: 1.0.2 resolution: "bintrees@npm:1.0.2" @@ -5531,7 +5550,7 @@ __metadata: languageName: node linkType: hard -"braces@npm:^3.0.3": +"braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" dependencies: @@ -5745,6 +5764,25 @@ __metadata: languageName: node linkType: hard +"chokidar@npm:^3.5.3": + version: 3.6.0 + resolution: "chokidar@npm:3.6.0" + dependencies: + anymatch: "npm:~3.1.2" + braces: "npm:~3.0.2" + fsevents: "npm:~2.3.2" + glob-parent: "npm:~5.1.2" + is-binary-path: "npm:~2.1.0" + is-glob: "npm:~4.0.1" + normalize-path: "npm:~3.0.0" + readdirp: "npm:~3.6.0" + dependenciesMeta: + fsevents: + optional: true + checksum: 10c0/8361dcd013f2ddbe260eacb1f3cb2f2c6f2b0ad118708a343a5ed8158941a39cb8fb1d272e0f389712e74ee90ce8ba864eece9e0e62b9705cb468a2f6d917462 + languageName: node + linkType: hard + "chokidar@npm:^4.0.0": version: 4.0.0 resolution: "chokidar@npm:4.0.0" @@ -7563,6 +7601,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:^11.3.0": + version: 11.3.0 + resolution: "fs-extra@npm:11.3.0" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10c0/5f95e996186ff45463059feb115a22fb048bdaf7e487ecee8a8646c78ed8fdca63630e3077d4c16ce677051f5e60d3355a06f3cd61f3ca43f48cc58822a44d0a + languageName: node + linkType: hard + "fs-extra@npm:^9.0.1": version: 9.1.0 resolution: "fs-extra@npm:9.1.0" @@ -7749,7 +7798,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^5.1.2": +"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": version: 5.1.2 resolution: "glob-parent@npm:5.1.2" dependencies: @@ -8116,6 +8165,13 @@ __metadata: languageName: node linkType: hard +"idb@npm:^8.0.3": + version: 8.0.3 + resolution: "idb@npm:8.0.3" + checksum: 10c0/421cd9a3281b7564528857031cc33fd9e95753f8191e483054cb25d1ceea7303a0d1462f4f69f5b41606f0f066156999e067478abf2460dfcf9cab80dae2a2b2 + languageName: node + linkType: hard + "ieee754@npm:^1.2.1": version: 1.2.1 resolution: "ieee754@npm:1.2.1" @@ -8319,6 +8375,15 @@ __metadata: languageName: node linkType: hard +"is-binary-path@npm:~2.1.0": + version: 2.1.0 + resolution: "is-binary-path@npm:2.1.0" + dependencies: + binary-extensions: "npm:^2.0.0" + checksum: 10c0/a16eaee59ae2b315ba36fad5c5dcaf8e49c3e27318f8ab8fa3cdb8772bf559c8d1ba750a589c2ccb096113bb64497084361a25960899cb6172a6925ab6123d38 + languageName: node + linkType: hard + "is-boolean-object@npm:^1.2.1": version: 1.2.2 resolution: "is-boolean-object@npm:1.2.2" @@ -8432,7 +8497,7 @@ __metadata: languageName: node linkType: hard -"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3": +"is-glob@npm:^4.0.0, is-glob@npm:^4.0.1, is-glob@npm:^4.0.3, is-glob@npm:~4.0.1": version: 4.0.3 resolution: "is-glob@npm:4.0.3" dependencies: @@ -9712,7 +9777,7 @@ __metadata: languageName: node linkType: hard -"normalize-path@npm:^3.0.0": +"normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" checksum: 10c0/e008c8142bcc335b5e38cf0d63cfd39d6cf2d97480af9abdbe9a439221fd4d749763bab492a8ee708ce7a194bb00c9da6d0a115018672310850489137b3da046 @@ -9927,6 +9992,13 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^7.0.3": + version: 7.0.3 + resolution: "p-map@npm:7.0.3" + checksum: 10c0/46091610da2b38ce47bcd1d8b4835a6fa4e832848a6682cf1652bc93915770f4617afc844c10a77d1b3e56d2472bb2d5622353fa3ead01a7f42b04fc8e744a5c + languageName: node + linkType: hard + "package-json-from-dist@npm:^1.0.0": version: 1.0.0 resolution: "package-json-from-dist@npm:1.0.0" @@ -10165,7 +10237,7 @@ __metadata: languageName: node linkType: hard -"picomatch@npm:^2.2.2, picomatch@npm:^2.3.1": +"picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.2, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" checksum: 10c0/26c02b8d06f03206fc2ab8d16f19960f2ff9e81a658f831ecb656d8f17d9edc799e8364b1f4a7873e89d9702dff96204be0fa26fe4181f6843f040f819dac4be @@ -11394,6 +11466,15 @@ __metadata: languageName: node linkType: hard +"readdirp@npm:~3.6.0": + version: 3.6.0 + resolution: "readdirp@npm:3.6.0" + dependencies: + picomatch: "npm:^2.2.1" + checksum: 10c0/6fa848cf63d1b82ab4e985f4cf72bd55b7dcfd8e0a376905804e48c3634b7e749170940ba77b32804d5fe93b3cc521aa95a8d7e7d725f830da6d93f3669ce66b + languageName: node + linkType: hard + "real-require@npm:^0.2.0": version: 0.2.0 resolution: "real-require@npm:0.2.0" @@ -13822,6 +13903,21 @@ __metadata: languageName: node linkType: hard +"vite-plugin-static-copy@npm:^3.1.0": + version: 3.1.0 + resolution: "vite-plugin-static-copy@npm:3.1.0" + dependencies: + chokidar: "npm:^3.5.3" + fs-extra: "npm:^11.3.0" + p-map: "npm:^7.0.3" + picocolors: "npm:^1.1.1" + tinyglobby: "npm:^0.2.14" + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + checksum: 10c0/dce43f12ecc71417f1afd530d15b316774fe0441c2502e48e2bfafcd07fd4ae90a5782621f932d8d12a8c8213bed6746e80d5452e2fb216ece2bcf7e80309f82 + languageName: node + linkType: hard + "vite-plugin-stimulus-hmr@npm:^3.0.0": version: 3.0.0 resolution: "vite-plugin-stimulus-hmr@npm:3.0.0" From e7c5c25de8c1bd4e32a4bf599c759303420c47e3 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 9 Jul 2025 14:13:51 +0200 Subject: [PATCH 07/10] Fix replying from media modal or pop-in-player tagging user `@undefined` (#35317) --- .../picture_in_picture/components/footer.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx index 26cb2172a9..080aaca451 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { defineMessages, useIntl } from 'react-intl'; @@ -21,6 +21,9 @@ import { openModal } from 'mastodon/actions/modal'; import { IconButton } from 'mastodon/components/icon_button'; import { useIdentity } from 'mastodon/identity_context'; import { me } from 'mastodon/initial_state'; +import type { Status } from 'mastodon/models/status'; +import { makeGetStatus } from 'mastodon/selectors'; +import type { RootState } from 'mastodon/store'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; const messages = defineMessages({ @@ -47,6 +50,11 @@ const messages = defineMessages({ open: { id: 'status.open', defaultMessage: 'Expand this status' }, }); +type GetStatusSelector = ( + state: RootState, + props: { id?: string | null; contextType?: string }, +) => Status | null; + export const Footer: React.FC<{ statusId: string; withOpenButton?: boolean; @@ -56,7 +64,8 @@ export const Footer: React.FC<{ const intl = useIntl(); const history = useHistory(); const dispatch = useAppDispatch(); - const status = useAppSelector((state) => state.statuses.get(statusId)); + const getStatus = useMemo(() => makeGetStatus(), []) as GetStatusSelector; + const status = useAppSelector((state) => getStatus(state, { id: statusId })); const accountId = status?.get('account') as string | undefined; const account = useAppSelector((state) => accountId ? state.accounts.get(accountId) : undefined, From 786b12e3799ba40e40b584a2daf4c9bc5cf4da60 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 9 Jul 2025 16:22:47 +0200 Subject: [PATCH 08/10] Relax error restriction in initializer (#35321) --- config/initializers/settings_digests.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/settings_digests.rb b/config/initializers/settings_digests.rb index 2a5d925c70..85599ee1b0 100644 --- a/config/initializers/settings_digests.rb +++ b/config/initializers/settings_digests.rb @@ -3,7 +3,7 @@ Rails.application.config.to_prepare do custom_css = begin Setting.custom_css - rescue ActiveRecord::AdapterError # Running without a database, not migrated, no connection, etc + rescue # Running without a cache, database, not migrated, no connection, etc nil end From 5cfc1fabcf6d95756571d3f31d66b4947bf21665 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 9 Jul 2025 16:34:16 +0200 Subject: [PATCH 09/10] Fix nearly every sub-directory being crawled as part of Vite build (#35323) --- vite.config.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.mts b/vite.config.mts index 6429f97c59..8d0fdfde51 100644 --- a/vite.config.mts +++ b/vite.config.mts @@ -83,7 +83,7 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => { format: 'es', }, plugins: [ - tsconfigPaths(), + tsconfigPaths({ projects: [path.resolve(__dirname, 'tsconfig.json')] }), RailsPlugin({ compress: mode === 'production' && command === 'build', sri: { From e9170e2de1d75e02f04ad39f2af131c70189fb50 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 9 Jul 2025 18:22:25 +0200 Subject: [PATCH 10/10] Bump version to v4.4.1 (#35329) --- CHANGELOG.md | 10 ++++++++++ docker-compose.yml | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a8a92682..19be8ea68e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## [4.4.1] - 2025-07-09 + +### Fixed + +- Fix nearly every sub-directory being crawled as part of Vite build (#35323 by @ClearlyClaire) +- Fix assets not building when Redis is unavailable (#35321 by @oneiros) +- Fix replying from media modal or pop-in-player tagging user `@undefined` (#35317 by @ClearlyClaire) +- Fix support for special characters in various environment variables (#35314 by @mjankowski and @ClearlyClaire) +- Fix some database migrations failing for indexes manually removed by admins (#35309 by @mjankowski) + ## [4.4.0] - 2025-07-08 ### Added diff --git a/docker-compose.yml b/docker-compose.yml index 4926101b95..e5d94b390f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.4.0 + image: ghcr.io/mastodon/mastodon:v4.4.1 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/mastodon/mastodon-streaming:v4.4.0 + image: ghcr.io/mastodon/mastodon-streaming:v4.4.1 restart: always env_file: .env.production command: node ./streaming/index.js @@ -102,7 +102,7 @@ services: sidekiq: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.4.0 + image: ghcr.io/mastodon/mastodon:v4.4.1 restart: always env_file: .env.production command: bundle exec sidekiq