From e955a580fa535ecc5149b18c36ebc8cf46838ff5 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski Date: Tue, 30 Jul 2024 14:52:01 +0200 Subject: [PATCH] Add a way for the user to select which languages they understand Closes #29989 --- .../settings/preferences/base_controller.rb | 2 +- .../mastodon/components/status_content.jsx | 8 ++++++-- .../compose/components/language_dropdown.jsx | 11 ++++++----- app/javascript/mastodon/initial_state.js | 1 + app/models/user.rb | 2 ++ app/serializers/initial_state_serializer.rb | 1 + app/views/settings/preferences/other/show.html.haml | 13 ++++++++++++- config/locales/en.yml | 2 +- config/locales/simple_form.en.yml | 4 +++- .../20240729134058_add_spoken_languages_to_users.rb | 7 +++++++ db/schema.rb | 3 ++- 11 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 db/migrate/20240729134058_add_spoken_languages_to_users.rb diff --git a/app/controllers/settings/preferences/base_controller.rb b/app/controllers/settings/preferences/base_controller.rb index c1f8b49898e..a7dc594a387 100644 --- a/app/controllers/settings/preferences/base_controller.rb +++ b/app/controllers/settings/preferences/base_controller.rb @@ -19,6 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController end def user_params - params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys) + params.require(:user).permit(:locale, :time_zone, spoken_languages: [], chosen_languages: [], settings_attributes: UserSettings.keys) end end diff --git a/app/javascript/mastodon/components/status_content.jsx b/app/javascript/mastodon/components/status_content.jsx index 96452374dcc..8b90f7e3253 100644 --- a/app/javascript/mastodon/components/status_content.jsx +++ b/app/javascript/mastodon/components/status_content.jsx @@ -13,7 +13,7 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react' import { Icon } from 'mastodon/components/icon'; import PollContainer from 'mastodon/containers/poll_container'; import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; -import { autoPlayGif, languages as preloadedLanguages } from 'mastodon/initial_state'; +import { autoPlayGif, spokenLanguages, languages as preloadedLanguages } from 'mastodon/initial_state'; const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top) @@ -237,6 +237,10 @@ class StatusContent extends PureComponent { this.node = c; }; + spokenByUser() { + return spokenLanguages.includes(this.props.status.get('language')); + }; + render () { const { status, intl, statusContent } = this.props; @@ -244,7 +248,7 @@ class StatusContent extends PureComponent { const renderReadMore = this.props.onClick && status.get('collapsed'); const contentLocale = intl.locale.replace(/[_-].*/, ''); const targetLanguages = this.props.languages?.get(status.get('language') || 'und'); - const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale); + const renderTranslate = this.props.onTranslate && this.props.identity.signedIn && !this.spokenByUser() && ['public', 'unlisted'].includes(status.get('visibility')) && status.get('search_index').trim().length > 0 && targetLanguages?.includes(contentLocale); const content = { __html: statusContent ?? getStatusContent(status) }; const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') }; diff --git a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx index 47e81cf134b..1ada317cf5a 100644 --- a/app/javascript/mastodon/features/compose/components/language_dropdown.jsx +++ b/app/javascript/mastodon/features/compose/components/language_dropdown.jsx @@ -13,7 +13,7 @@ import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react'; import SearchIcon from '@/material-icons/400-24px/search.svg?react'; import TranslateIcon from '@/material-icons/400-24px/translate.svg?react'; import { Icon } from 'mastodon/components/icon'; -import { languages as preloadedLanguages } from 'mastodon/initial_state'; +import { spokenLanguages, languages as preloadedLanguages } from 'mastodon/initial_state'; const messages = defineMessages({ changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' }, @@ -84,6 +84,9 @@ class LanguageDropdownMenu extends PureComponent { const { languages, value, frequentlyUsedLanguages } = this.props; const { searchValue } = this.state; + // first show spoken languages and then frequently used + const orderedLanguages = spokenLanguages.concat(frequentlyUsedLanguages.filter((item) => spokenLanguages.indexOf(item) < 0)); + if (searchValue === '') { return [...languages].sort((a, b) => { // Push current selection to the top of the list @@ -93,10 +96,8 @@ class LanguageDropdownMenu extends PureComponent { } else if (b[0] === value) { return 1; } else { - // Sort according to frequently used languages - - const indexOfA = frequentlyUsedLanguages.indexOf(a[0]); - const indexOfB = frequentlyUsedLanguages.indexOf(b[0]); + const indexOfA = orderedLanguages.indexOf(a[0]); + const indexOfB = orderedLanguages.indexOf(b[0]); return ((indexOfA > -1 ? indexOfA : Infinity) - (indexOfB > -1 ? indexOfB : Infinity)); } diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 60b35cb31ac..b0a7cfa6456 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -118,6 +118,7 @@ export const criticalUpdatesPending = initialState?.critical_updates_pending; // @ts-expect-error export const statusPageUrl = getMeta('status_page_url'); export const sso_redirect = getMeta('sso_redirect'); +export const spokenLanguages = getMeta('spoken_languages'); /** * @returns {string | undefined} diff --git a/app/models/user.rb b/app/models/user.rb index 72854569260..95a744a53c9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,6 +40,7 @@ # settings :text # time_zone :string # otp_secret :string +# spoken_languages :string default([]), not null, is an Array # class User < ApplicationRecord @@ -134,6 +135,7 @@ class User < ApplicationRecord normalizes :locale, with: ->(locale) { I18n.available_locales.exclude?(locale.to_sym) ? nil : locale } normalizes :time_zone, with: ->(time_zone) { ActiveSupport::TimeZone[time_zone].nil? ? nil : time_zone } normalizes :chosen_languages, with: ->(chosen_languages) { chosen_languages.compact_blank.presence } + normalizes :spoken_languages, with: ->(spoken_languages) { spoken_languages.compact_blank } has_many :session_activations, dependent: :destroy diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 13f332c95c4..bb8a6bd5ce2 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -29,6 +29,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:use_blurhash] = object_account_user.setting_use_blurhash store[:use_pending_items] = object_account_user.setting_use_pending_items store[:show_trends] = Setting.trends && object_account_user.setting_trends + store[:spoken_languages] = object_account_user.spoken_languages else store[:auto_play_gif] = Setting.auto_play_gif store[:display_media] = Setting.display_media diff --git a/app/views/settings/preferences/other/show.html.haml b/app/views/settings/preferences/other/show.html.haml index e2888f72129..142f05ec00b 100644 --- a/app/views/settings/preferences/other/show.html.haml +++ b/app/views/settings/preferences/other/show.html.haml @@ -44,7 +44,7 @@ label: I18n.t('simple_form.labels.defaults.setting_default_sensitive'), wrapper: :with_label - %h4= t 'preferences.public_timelines' + %h4= t 'preferences.languages' .fields-group = f.input :chosen_languages, @@ -57,5 +57,16 @@ required: false, wrapper: :with_block_label + .fields-group + = f.input :spoken_languages, + as: :check_boxes, + collection_wrapper_tag: 'ul', + collection: filterable_languages, + include_blank: false, + item_wrapper_tag: 'li', + label_method: ->(locale) { native_locale_name(locale) }, + required: false, + wrapper: :with_block_label + .actions = f.button :button, t('generic.save_changes'), type: :submit diff --git a/config/locales/en.yml b/config/locales/en.yml index 322183f4ce9..3ec28036d3a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1544,9 +1544,9 @@ en: too_few_options: must have more than one item too_many_options: can't contain more than %{max} items preferences: + languages: Languages other: Other posting_defaults: Posting defaults - public_timelines: Public timelines privacy: hint_html: "Customize how you want your profile and your posts to be found. A variety of features in Mastodon can help you reach a wider audience when enabled. Take a moment to review these settings to make sure they fit your use case." privacy: Privacy diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 6bc7c6ac525..9d7508b4bf8 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -131,6 +131,7 @@ en: user: chosen_languages: When checked, only posts in selected languages will be displayed in public timelines role: The role controls which permissions the user has + spoken_languages: Mastodon will not suggest to translate statuses in the languages that you speak user_role: color: Color to be used for the role throughout the UI, as RGB in hex format highlighted: This makes the role publicly visible @@ -181,7 +182,7 @@ en: autofollow: Invite to follow your account avatar: Profile picture bot: This is an automated account - chosen_languages: Filter languages + chosen_languages: Filter languages in public timelines confirm_new_password: Confirm new password confirm_password: Confirm password context: Filter contexts @@ -228,6 +229,7 @@ en: setting_use_pending_items: Slow mode severity: Severity sign_in_token_attempt: Security code + spoken_languages: Languages that you can speak title: Title type: Import type username: Username diff --git a/db/migrate/20240729134058_add_spoken_languages_to_users.rb b/db/migrate/20240729134058_add_spoken_languages_to_users.rb new file mode 100644 index 00000000000..ca44e6ad588 --- /dev/null +++ b/db/migrate/20240729134058_add_spoken_languages_to_users.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddSpokenLanguagesToUsers < ActiveRecord::Migration[7.1] + def change + add_column :users, :spoken_languages, :string, array: true, null: false, default: [] + end +end diff --git a/db/schema.rb b/db/schema.rb index d4796079cae..8c9e89fac89 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_07_24_181224) do +ActiveRecord::Schema[7.1].define(version: 2024_07_29_134058) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1205,6 +1205,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_24_181224) do t.text "settings" t.string "time_zone" t.string "otp_secret" + t.string "spoken_languages", default: [], null: false, array: true t.index ["account_id"], name: "index_users_on_account_id" t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"