mirror of
https://github.com/mastodon/mastodon.git
synced 2024-08-20 21:08:15 -07:00
Add a way for the user to select which languages they understand
Closes #29989
This commit is contained in:
parent
dfd43869c9
commit
e955a580fa
11 changed files with 42 additions and 12 deletions
|
@ -19,6 +19,6 @@ class Settings::Preferences::BaseController < Settings::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react'
|
||||||
import { Icon } from 'mastodon/components/icon';
|
import { Icon } from 'mastodon/components/icon';
|
||||||
import PollContainer from 'mastodon/containers/poll_container';
|
import PollContainer from 'mastodon/containers/poll_container';
|
||||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
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)
|
const MAX_HEIGHT = 706; // 22px * 32 (+ 2px padding at the top)
|
||||||
|
@ -237,6 +237,10 @@ class StatusContent extends PureComponent {
|
||||||
this.node = c;
|
this.node = c;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
spokenByUser() {
|
||||||
|
return spokenLanguages.includes(this.props.status.get('language'));
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl, statusContent } = this.props;
|
const { status, intl, statusContent } = this.props;
|
||||||
|
|
||||||
|
@ -244,7 +248,7 @@ class StatusContent extends PureComponent {
|
||||||
const renderReadMore = this.props.onClick && status.get('collapsed');
|
const renderReadMore = this.props.onClick && status.get('collapsed');
|
||||||
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
const contentLocale = intl.locale.replace(/[_-].*/, '');
|
||||||
const targetLanguages = this.props.languages?.get(status.get('language') || 'und');
|
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 content = { __html: statusContent ?? getStatusContent(status) };
|
||||||
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
|
const spoilerContent = { __html: status.getIn(['translation', 'spoilerHtml']) || status.get('spoilerHtml') };
|
||||||
|
|
|
@ -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 SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||||
import TranslateIcon from '@/material-icons/400-24px/translate.svg?react';
|
import TranslateIcon from '@/material-icons/400-24px/translate.svg?react';
|
||||||
import { Icon } from 'mastodon/components/icon';
|
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({
|
const messages = defineMessages({
|
||||||
changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
|
changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' },
|
||||||
|
@ -84,6 +84,9 @@ class LanguageDropdownMenu extends PureComponent {
|
||||||
const { languages, value, frequentlyUsedLanguages } = this.props;
|
const { languages, value, frequentlyUsedLanguages } = this.props;
|
||||||
const { searchValue } = this.state;
|
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 === '') {
|
if (searchValue === '') {
|
||||||
return [...languages].sort((a, b) => {
|
return [...languages].sort((a, b) => {
|
||||||
// Push current selection to the top of the list
|
// Push current selection to the top of the list
|
||||||
|
@ -93,10 +96,8 @@ class LanguageDropdownMenu extends PureComponent {
|
||||||
} else if (b[0] === value) {
|
} else if (b[0] === value) {
|
||||||
return 1;
|
return 1;
|
||||||
} else {
|
} else {
|
||||||
// Sort according to frequently used languages
|
const indexOfA = orderedLanguages.indexOf(a[0]);
|
||||||
|
const indexOfB = orderedLanguages.indexOf(b[0]);
|
||||||
const indexOfA = frequentlyUsedLanguages.indexOf(a[0]);
|
|
||||||
const indexOfB = frequentlyUsedLanguages.indexOf(b[0]);
|
|
||||||
|
|
||||||
return ((indexOfA > -1 ? indexOfA : Infinity) - (indexOfB > -1 ? indexOfB : Infinity));
|
return ((indexOfA > -1 ? indexOfA : Infinity) - (indexOfB > -1 ? indexOfB : Infinity));
|
||||||
}
|
}
|
||||||
|
|
|
@ -118,6 +118,7 @@ export const criticalUpdatesPending = initialState?.critical_updates_pending;
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
export const statusPageUrl = getMeta('status_page_url');
|
export const statusPageUrl = getMeta('status_page_url');
|
||||||
export const sso_redirect = getMeta('sso_redirect');
|
export const sso_redirect = getMeta('sso_redirect');
|
||||||
|
export const spokenLanguages = getMeta('spoken_languages');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {string | undefined}
|
* @returns {string | undefined}
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
# settings :text
|
# settings :text
|
||||||
# time_zone :string
|
# time_zone :string
|
||||||
# otp_secret :string
|
# otp_secret :string
|
||||||
|
# spoken_languages :string default([]), not null, is an Array
|
||||||
#
|
#
|
||||||
|
|
||||||
class User < ApplicationRecord
|
class User < ApplicationRecord
|
||||||
|
@ -134,6 +135,7 @@ class User < ApplicationRecord
|
||||||
normalizes :locale, with: ->(locale) { I18n.available_locales.exclude?(locale.to_sym) ? nil : locale }
|
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 :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 :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
|
has_many :session_activations, dependent: :destroy
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ class InitialStateSerializer < ActiveModel::Serializer
|
||||||
store[:use_blurhash] = object_account_user.setting_use_blurhash
|
store[:use_blurhash] = object_account_user.setting_use_blurhash
|
||||||
store[:use_pending_items] = object_account_user.setting_use_pending_items
|
store[:use_pending_items] = object_account_user.setting_use_pending_items
|
||||||
store[:show_trends] = Setting.trends && object_account_user.setting_trends
|
store[:show_trends] = Setting.trends && object_account_user.setting_trends
|
||||||
|
store[:spoken_languages] = object_account_user.spoken_languages
|
||||||
else
|
else
|
||||||
store[:auto_play_gif] = Setting.auto_play_gif
|
store[:auto_play_gif] = Setting.auto_play_gif
|
||||||
store[:display_media] = Setting.display_media
|
store[:display_media] = Setting.display_media
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
label: I18n.t('simple_form.labels.defaults.setting_default_sensitive'),
|
label: I18n.t('simple_form.labels.defaults.setting_default_sensitive'),
|
||||||
wrapper: :with_label
|
wrapper: :with_label
|
||||||
|
|
||||||
%h4= t 'preferences.public_timelines'
|
%h4= t 'preferences.languages'
|
||||||
|
|
||||||
.fields-group
|
.fields-group
|
||||||
= f.input :chosen_languages,
|
= f.input :chosen_languages,
|
||||||
|
@ -57,5 +57,16 @@
|
||||||
required: false,
|
required: false,
|
||||||
wrapper: :with_block_label
|
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
|
.actions
|
||||||
= f.button :button, t('generic.save_changes'), type: :submit
|
= f.button :button, t('generic.save_changes'), type: :submit
|
||||||
|
|
|
@ -1544,9 +1544,9 @@ en:
|
||||||
too_few_options: must have more than one item
|
too_few_options: must have more than one item
|
||||||
too_many_options: can't contain more than %{max} items
|
too_many_options: can't contain more than %{max} items
|
||||||
preferences:
|
preferences:
|
||||||
|
languages: Languages
|
||||||
other: Other
|
other: Other
|
||||||
posting_defaults: Posting defaults
|
posting_defaults: Posting defaults
|
||||||
public_timelines: Public timelines
|
|
||||||
privacy:
|
privacy:
|
||||||
hint_html: "<strong>Customize how you want your profile and your posts to be found.</strong> 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."
|
hint_html: "<strong>Customize how you want your profile and your posts to be found.</strong> 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
|
privacy: Privacy
|
||||||
|
|
|
@ -131,6 +131,7 @@ en:
|
||||||
user:
|
user:
|
||||||
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
|
chosen_languages: When checked, only posts in selected languages will be displayed in public timelines
|
||||||
role: The role controls which permissions the user has
|
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:
|
user_role:
|
||||||
color: Color to be used for the role throughout the UI, as RGB in hex format
|
color: Color to be used for the role throughout the UI, as RGB in hex format
|
||||||
highlighted: This makes the role publicly visible
|
highlighted: This makes the role publicly visible
|
||||||
|
@ -181,7 +182,7 @@ en:
|
||||||
autofollow: Invite to follow your account
|
autofollow: Invite to follow your account
|
||||||
avatar: Profile picture
|
avatar: Profile picture
|
||||||
bot: This is an automated account
|
bot: This is an automated account
|
||||||
chosen_languages: Filter languages
|
chosen_languages: Filter languages in public timelines
|
||||||
confirm_new_password: Confirm new password
|
confirm_new_password: Confirm new password
|
||||||
confirm_password: Confirm password
|
confirm_password: Confirm password
|
||||||
context: Filter contexts
|
context: Filter contexts
|
||||||
|
@ -228,6 +229,7 @@ en:
|
||||||
setting_use_pending_items: Slow mode
|
setting_use_pending_items: Slow mode
|
||||||
severity: Severity
|
severity: Severity
|
||||||
sign_in_token_attempt: Security code
|
sign_in_token_attempt: Security code
|
||||||
|
spoken_languages: Languages that you can speak
|
||||||
title: Title
|
title: Title
|
||||||
type: Import type
|
type: Import type
|
||||||
username: Username
|
username: Username
|
||||||
|
|
|
@ -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
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
|
@ -1205,6 +1205,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_24_181224) do
|
||||||
t.text "settings"
|
t.text "settings"
|
||||||
t.string "time_zone"
|
t.string "time_zone"
|
||||||
t.string "otp_secret"
|
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 ["account_id"], name: "index_users_on_account_id"
|
||||||
t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
|
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)"
|
t.index ["created_by_application_id"], name: "index_users_on_created_by_application_id", where: "(created_by_application_id IS NOT NULL)"
|
||||||
|
|
Loading…
Reference in a new issue