discourse/app/models/translation_override.rb

231 lines
6.8 KiB
Ruby

# frozen_string_literal: true
class TranslationOverride < ActiveRecord::Base
# TODO: Remove once
# 20240711123755_drop_compiled_js_from_translation_overrides has been
# promoted to pre-deploy
self.ignored_columns = %w[compiled_js]
# Allowlist i18n interpolation keys that can be included when customizing translations
ALLOWED_CUSTOM_INTERPOLATION_KEYS = {
%w[
user_notifications.user_
user_notifications.only_reply_by_email
user_notifications.reply_by_email
user_notifications.visit_link_to_respond
user_notifications.header_instructions
user_notifications.pm_participants
unsubscribe_mailing_list
unsubscribe_link_and_mail
unsubscribe_link
] => %w[
topic_title
topic_title_url_encoded
message
url
post_id
topic_id
context
username
group_name
unsubscribe_url
subject_pm
participants
site_description
site_title
site_title_url_encoded
site_name
optional_re
optional_pm
optional_cat
optional_tags
],
%w[system_messages.welcome_user] => %w[username name name_or_username],
}
include HasSanitizableFields
validates_uniqueness_of :translation_key, scope: :locale
validates_presence_of :locale, :translation_key, :value
validate :check_interpolation_keys
validate :check_MF_string, if: :message_format?
attribute :status, :integer
enum :status, { up_to_date: 0, outdated: 1, invalid_interpolation_keys: 2, deprecated: 3 }
scope :mf_locales,
->(locale) { not_deprecated.where(locale: locale).where("translation_key LIKE '%_MF'") }
scope :client_locales,
->(locale) do
not_deprecated
.where(locale: locale)
.where("translation_key LIKE 'js.%' OR translation_key LIKE 'admin_js.%'")
.where.not("translation_key LIKE '%_MF'")
end
def self.upsert!(locale, key, value)
params = { locale: locale, translation_key: key }
translation_override = find_or_initialize_by(params)
sanitized_value =
translation_override.sanitize_field(value, additional_attributes: %w[data-auto-route target])
original_translation =
I18n.overrides_disabled { I18n.t(transform_pluralized_key(key), locale: :en) }
data = { value: sanitized_value, original_translation: original_translation }
params.merge!(data) if translation_override.new_record?
i18n_changed(locale, [key]) if translation_override.update(data)
translation_override
end
def self.revert!(locale, keys)
keys = Array.wrap(keys)
TranslationOverride.where(locale: locale, translation_key: keys).delete_all
i18n_changed(locale, keys)
end
def self.reload_all_overrides!
reload_locale!
overrides = TranslationOverride.pluck(:locale, :translation_key)
overrides = overrides.group_by(&:first).map { |k, a| [k, a.map(&:last)] }
overrides.each { |locale, keys| clear_cached_keys!(locale, keys) }
end
def self.reload_locale!
I18n.reload!
ExtraLocalesController.clear_cache!
MessageBus.publish("/i18n-flush", refresh: true)
end
def self.clear_cached_keys!(locale, keys)
should_clear_anon_cache = false
keys.each { |key| should_clear_anon_cache |= expire_cache(locale, key) }
Site.clear_anon_cache! if should_clear_anon_cache
end
def self.i18n_changed(locale, keys)
reload_locale!
clear_cached_keys!(locale, keys)
end
def self.expire_cache(locale, key)
if key.starts_with?("post_action_types.") || key.starts_with?("topic_flag_types.")
PostActionType.new.expire_cache
else
return false
end
true
end
# We use English as the source of truth when extracting interpolation keys,
# but some languages, like Arabic, have plural forms (zero, two, few, many)
# which don't exist in English (one, other), so we map that here in order to
# find the correct, English translation key in which to look.
def self.transform_pluralized_key(key)
match = key.match(/(.*)\.(zero|two|few|many)\z/)
match ? match.to_a.second + ".other" : key
end
def self.custom_interpolation_keys(translation_key)
ALLOWED_CUSTOM_INTERPOLATION_KEYS.find do |keys, value|
break value if keys.any? { |k| translation_key.start_with?(k) }
end || []
end
private_class_method :reload_locale!
private_class_method :clear_cached_keys!
private_class_method :i18n_changed
private_class_method :expire_cache
def original_translation_deleted?
!I18n.overrides_disabled { I18n.t!(transformed_key, locale: :en) }.is_a?(String)
rescue I18n::MissingTranslationData
true
end
def original_translation_updated?
return false if original_translation.blank?
original_translation != current_default
end
def invalid_interpolation_keys
return [] if current_default.blank?
original_interpolation_keys = I18nInterpolationKeysFinder.find(current_default)
new_interpolation_keys = I18nInterpolationKeysFinder.find(value)
custom_interpolation_keys = []
ALLOWED_CUSTOM_INTERPOLATION_KEYS.select do |keys, value|
custom_interpolation_keys = value if keys.any? { |key| transformed_key.start_with?(key) }
end
(original_interpolation_keys | new_interpolation_keys) - original_interpolation_keys -
custom_interpolation_keys
end
def current_default
I18n.overrides_disabled { I18n.t(transformed_key, locale: :en) }
end
def message_format?
translation_key.to_s.end_with?("_MF")
end
def make_up_to_date!
return unless outdated?
self.original_translation = current_default
update_attribute!(:status, :up_to_date)
end
private
def transformed_key
@transformed_key ||= self.class.transform_pluralized_key(translation_key)
end
def check_interpolation_keys
invalid_keys = invalid_interpolation_keys
return if invalid_keys.blank?
self.errors.add(
:base,
I18n.t(
"activerecord.errors.models.translation_overrides.attributes.value.invalid_interpolation_keys",
keys: invalid_keys.join(I18n.t("word_connector.comma")),
count: invalid_keys.size,
),
)
end
def check_MF_string
require "messageformat"
MessageFormat.compile(locale, { key: value }, strict: true)
rescue MessageFormat::Compiler::CompileError => e
errors.add(:base, e.cause.message)
end
end
# == Schema Information
#
# Table name: translation_overrides
#
# id :integer not null, primary key
# locale :string not null
# translation_key :string not null
# value :string not null
# created_at :datetime not null
# updated_at :datetime not null
# original_translation :text
# status :integer default("up_to_date"), not null
#
# Indexes
#
# index_translation_overrides_on_locale_and_translation_key (locale,translation_key) UNIQUE
#