Add ability to translate server rules (#34494)

This commit is contained in:
Claire 2025-05-21 13:54:12 +02:00 committed by GitHub
parent 977164decc
commit 8c51a8ba94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 149 additions and 20 deletions

View file

@ -7,7 +7,7 @@ module Admin
def index
authorize :rule, :index?
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
end
def new
@ -27,7 +27,6 @@ module Admin
if @rule.save
redirect_to admin_rules_path
else
@rules = Rule.ordered
render :new
end
end
@ -74,7 +73,7 @@ module Admin
def resource_params
params
.expect(rule: [:text, :hint, :priority])
.expect(rule: [:text, :hint, :priority, translations_attributes: [[:id, :language, :text, :hint, :_destroy]]])
end
end
end

View file

@ -18,6 +18,6 @@ class Api::V1::Instances::RulesController < Api::V1::Instances::BaseController
private
def set_rules
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
end
end

View file

@ -126,7 +126,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
end
def set_rules
@rules = Rule.ordered
@rules = Rule.ordered.includes(:translations)
end
def require_rules_acceptance!

View file

@ -44,6 +44,7 @@ const severityMessages = {
const mapStateToProps = state => ({
server: state.getIn(['server', 'server']),
locale: state.getIn(['meta', 'locale']),
extendedDescription: state.getIn(['server', 'extendedDescription']),
domainBlocks: state.getIn(['server', 'domainBlocks']),
});
@ -91,6 +92,7 @@ class About extends PureComponent {
static propTypes = {
server: ImmutablePropTypes.map,
locale: ImmutablePropTypes.string,
extendedDescription: ImmutablePropTypes.map,
domainBlocks: ImmutablePropTypes.contains({
isLoading: PropTypes.bool,
@ -114,7 +116,7 @@ class About extends PureComponent {
};
render () {
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;
const { multiColumn, intl, server, extendedDescription, domainBlocks, locale } = this.props;
const isLoading = server.get('isLoading');
return (
@ -168,12 +170,15 @@ class About extends PureComponent {
<p><FormattedMessage id='about.not_available' defaultMessage='This information has not been made available on this server.' /></p>
) : (
<ol className='rules-list'>
{server.get('rules').map(rule => (
<li key={rule.get('id')}>
<div className='rules-list__text'>{rule.get('text')}</div>
{rule.get('hint').length > 0 && (<div className='rules-list__hint'>{rule.get('hint')}</div>)}
</li>
))}
{server.get('rules').map(rule => {
const text = rule.getIn(['translations', locale, 'text']) || rule.get('text');
const hint = rule.getIn(['translations', locale, 'hint']) || rule.get('hint');
return (
<li key={rule.get('id')}>
<div className='rules-list__text'>{text}</div>
{hint.length > 0 && (<div className='rules-list__hint'>{hint}</div>)}
</li>
)})}
</ol>
))}
</Section>

View file

@ -12,6 +12,7 @@ import Option from './components/option';
const mapStateToProps = state => ({
rules: state.getIn(['server', 'server', 'rules']),
locale: state.getIn(['meta', 'locale']),
});
class Rules extends PureComponent {
@ -19,6 +20,7 @@ class Rules extends PureComponent {
static propTypes = {
onNextStep: PropTypes.func.isRequired,
rules: ImmutablePropTypes.list,
locale: PropTypes.string,
selectedRuleIds: ImmutablePropTypes.set.isRequired,
onToggle: PropTypes.func.isRequired,
};
@ -34,7 +36,7 @@ class Rules extends PureComponent {
};
render () {
const { rules, selectedRuleIds } = this.props;
const { rules, locale, selectedRuleIds } = this.props;
return (
<>
@ -49,7 +51,7 @@ class Rules extends PureComponent {
value={item.get('id')}
checked={selectedRuleIds.includes(item.get('id'))}
onToggle={this.handleRulesToggle}
label={item.get('text')}
label={item.getIn(['translations', locale, 'text']) || item.get('text')}
multiple
/>
))}

View file

@ -19,6 +19,9 @@ class Rule < ApplicationRecord
self.discard_column = :deleted_at
has_many :translations, inverse_of: :rule, class_name: 'RuleTranslation', dependent: :destroy
accepts_nested_attributes_for :translations, reject_if: :all_blank, allow_destroy: true
validates :text, presence: true, length: { maximum: TEXT_SIZE_LIMIT }
scope :ordered, -> { kept.order(priority: :asc, id: :asc) }
@ -36,4 +39,9 @@ class Rule < ApplicationRecord
end
end
end
def translation_for(locale)
@cached_translations ||= {}
@cached_translations[locale] ||= translations.find_by(language: locale) || RuleTranslation.new(language: locale, text: text, hint: hint)
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: rule_translations
#
# id :bigint(8) not null, primary key
# hint :text default(""), not null
# language :string not null
# text :text default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
# rule_id :bigint(8) not null
#
class RuleTranslation < ApplicationRecord
belongs_to :rule
validates :language, presence: true, uniqueness: { scope: :rule_id }
validates :text, presence: true, length: { maximum: Rule::TEXT_SIZE_LIMIT }
end

View file

@ -47,7 +47,7 @@ class InstancePresenter < ActiveModelSerializers::Model
end
def rules
Rule.ordered
Rule.ordered.includes(:translations)
end
def user_count

View file

@ -1,9 +1,15 @@
# frozen_string_literal: true
class REST::RuleSerializer < ActiveModel::Serializer
attributes :id, :text, :hint
attributes :id, :text, :hint, :translations
def id
object.id.to_s
end
def translations
object.translations.to_h do |translation|
[translation.language, { text: translation.text, hint: translation.hint }]
end
end
end

View file

@ -0,0 +1,27 @@
%tr.nested-fields
%td
.fields-row
.fields-row__column.fields-group
= f.input :language,
collection: ui_languages,
include_blank: false,
label_method: ->(locale) { native_locale_name(locale) }
.fields-row__column.fields-group
= f.hidden_field :id if f.object&.persisted? # Required so Rails doesn't put the field outside of the <tr/>
= link_to_remove_association(f, class: 'table-action-link') do
= safe_join([material_symbol('close'), t('filters.index.delete')])
.fields-group
= f.input :text,
label: I18n.t('simple_form.labels.rule.text'),
hint: I18n.t('simple_form.hints.rule.text'),
input_html: { lang: f.object&.language },
wrapper: :with_block_label
.fields-group
= f.input :hint,
label: I18n.t('simple_form.labels.rule.hint'),
hint: I18n.t('simple_form.hints.rule.hint'),
input_html: { lang: f.object&.language },
wrapper: :with_block_label

View file

@ -6,5 +6,26 @@
= render form
%hr.spacer/
%h4= t('admin.rules.translations')
%p.hint= t('admin.rules.translations_explanation')
.table-wrapper
%table.table.keywords-table
%thead
%tr
%th= t('admin.rules.translation')
%th
%tbody
= form.simple_fields_for :translations do |translation|
= render 'translation_fields', f: translation
%tfoot
%tr
%td{ colspan: 3 }
= link_to_add_association form, :translations, class: 'table-action-link', partial: 'translation_fields', 'data-association-insertion-node': '.keywords-table tbody', 'data-association-insertion-method': 'append' do
= safe_join([material_symbol('add'), t('admin.rules.add_translation')])
.actions
= form.button :button, t('generic.save_changes'), type: :submit

View file

@ -18,10 +18,11 @@
%ol.rules-list
- @rules.each do |rule|
- translation = rule.translation_for(I18n.locale.to_s)
%li
%button{ type: 'button', aria: { expanded: 'false' } }
.rules-list__text= rule.text
.rules-list__hint= rule.hint
.rules-list__text= translation.text
.rules-list__hint= translation.hint
.stacked-actions
- accept_path = @invite_code.present? ? public_invite_url(invite_code: @invite_code, accept: @accept_token) : new_user_registration_path(accept: @accept_token)