2019-05-02 17:17:27 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2015-11-13 15:34:13 -06:00
|
|
|
class TranslationOverride < ActiveRecord::Base
|
2020-07-26 19:23:54 -05:00
|
|
|
# Allowlist i18n interpolation keys that can be included when customizing translations
|
2020-09-27 22:52:05 -05:00
|
|
|
ALLOWED_CUSTOM_INTERPOLATION_KEYS = {
|
2021-03-24 14:58:24 -05:00
|
|
|
%w[
|
|
|
|
user_notifications.user_
|
|
|
|
user_notifications.only_reply_by_email
|
|
|
|
user_notifications.reply_by_email
|
|
|
|
user_notifications.visit_link_to_respond
|
2021-03-25 13:00:05 -05:00
|
|
|
user_notifications.header_instructions
|
|
|
|
user_notifications.pm_participants
|
|
|
|
unsubscribe_mailing_list
|
|
|
|
unsubscribe_link_and_mail
|
|
|
|
unsubscribe_link
|
2021-03-24 14:58:24 -05:00
|
|
|
] => %w[
|
2021-03-25 13:00:05 -05:00
|
|
|
topic_title
|
2017-11-20 02:10:06 -06:00
|
|
|
topic_title_url_encoded
|
2021-03-25 13:00:05 -05:00
|
|
|
message
|
|
|
|
url
|
|
|
|
post_id
|
|
|
|
topic_id
|
2017-11-20 02:10:06 -06:00
|
|
|
context
|
2021-03-25 13:00:05 -05:00
|
|
|
username
|
|
|
|
group_name
|
|
|
|
unsubscribe_url
|
|
|
|
subject_pm
|
|
|
|
participants
|
|
|
|
site_description
|
|
|
|
site_title
|
|
|
|
site_title_url_encoded
|
2020-11-02 22:00:11 -06:00
|
|
|
site_name
|
|
|
|
optional_re
|
|
|
|
optional_pm
|
|
|
|
optional_cat
|
|
|
|
optional_tags
|
2023-01-09 06:20:10 -06:00
|
|
|
],
|
2023-02-17 15:38:21 -06:00
|
|
|
%w[system_messages.welcome_user] => %w[username name name_or_username],
|
2017-11-20 02:10:06 -06:00
|
|
|
}
|
2021-03-24 14:58:24 -05:00
|
|
|
|
2021-10-27 09:33:07 -05:00
|
|
|
include HasSanitizableFields
|
2017-11-20 02:10:06 -06:00
|
|
|
|
2015-11-13 15:34:13 -06:00
|
|
|
validates_uniqueness_of :translation_key, scope: :locale
|
|
|
|
validates_presence_of :locale, :translation_key, :value
|
|
|
|
|
2017-06-15 03:08:23 -05:00
|
|
|
validate :check_interpolation_keys
|
|
|
|
|
2024-05-27 05:27:13 -05:00
|
|
|
enum status: { up_to_date: 0, outdated: 1, invalid_interpolation_keys: 2, deprecated: 3 }
|
2023-07-09 21:06:40 -05:00
|
|
|
|
2015-11-13 15:34:13 -06:00
|
|
|
def self.upsert!(locale, key, value)
|
|
|
|
params = { locale: locale, translation_key: key }
|
2016-04-08 13:49:50 -05:00
|
|
|
|
2021-10-27 09:33:07 -05:00
|
|
|
translation_override = find_or_initialize_by(params)
|
|
|
|
sanitized_value =
|
|
|
|
translation_override.sanitize_field(value, additional_attributes: ["data-auto-route"])
|
2023-07-09 21:06:40 -05:00
|
|
|
original_translation =
|
|
|
|
I18n.overrides_disabled { I18n.t(transform_pluralized_key(key), locale: :en) }
|
2021-10-27 09:33:07 -05:00
|
|
|
|
2023-07-09 21:06:40 -05:00
|
|
|
data = { value: sanitized_value, original_translation: original_translation }
|
2016-04-08 13:49:50 -05:00
|
|
|
if key.end_with?("_MF")
|
2019-02-19 07:42:58 -06:00
|
|
|
_, filename = JsLocaleHelper.find_message_format_locale([locale], fallback_to_english: false)
|
2021-10-27 09:33:07 -05:00
|
|
|
data[:compiled_js] = JsLocaleHelper.compile_message_format(filename, locale, sanitized_value)
|
2016-04-08 13:49:50 -05:00
|
|
|
end
|
|
|
|
|
2017-06-15 03:08:23 -05:00
|
|
|
params.merge!(data) if translation_override.new_record?
|
2020-05-18 11:05:34 -05:00
|
|
|
i18n_changed(locale, [key]) if translation_override.update(data)
|
2017-06-15 03:08:23 -05:00
|
|
|
translation_override
|
2015-11-20 11:30:04 -06:00
|
|
|
end
|
2015-11-17 15:14:42 -06:00
|
|
|
|
2020-05-18 11:05:34 -05:00
|
|
|
def self.revert!(locale, keys)
|
|
|
|
keys = Array.wrap(keys)
|
2015-11-20 11:30:04 -06:00
|
|
|
TranslationOverride.where(locale: locale, translation_key: keys).delete_all
|
2020-05-18 11:05:34 -05:00
|
|
|
i18n_changed(locale, keys)
|
2015-11-13 15:34:13 -06:00
|
|
|
end
|
|
|
|
|
2020-05-18 11:45:47 -05:00
|
|
|
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
|
|
|
|
|
2020-05-18 11:05:34 -05:00
|
|
|
def self.reload_locale!
|
2018-03-16 15:46:49 -05:00
|
|
|
I18n.reload!
|
2019-11-04 19:15:44 -06:00
|
|
|
ExtraLocalesController.clear_cache!
|
2018-03-16 15:46:49 -05:00
|
|
|
MessageBus.publish("/i18n-flush", refresh: true)
|
2020-05-18 11:05:34 -05:00
|
|
|
end
|
2015-11-20 11:30:04 -06:00
|
|
|
|
2020-05-18 11:05:34 -05:00
|
|
|
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)
|
2018-03-16 15:46:49 -05:00
|
|
|
end
|
2015-11-20 11:30:04 -06:00
|
|
|
|
2020-05-18 11:05:34 -05:00
|
|
|
def self.expire_cache(locale, key)
|
2018-03-16 15:46:49 -05:00
|
|
|
if key.starts_with?("post_action_types.")
|
2020-05-18 11:05:34 -05:00
|
|
|
ApplicationSerializer.expire_cache_fragment!("post_action_types_#{locale}")
|
2018-03-16 15:46:49 -05:00
|
|
|
elsif key.starts_with?("topic_flag_types.")
|
2020-05-18 11:05:34 -05:00
|
|
|
ApplicationSerializer.expire_cache_fragment!("post_action_flag_types_#{locale}")
|
2018-03-16 15:46:49 -05:00
|
|
|
else
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
true
|
|
|
|
end
|
2017-06-15 03:08:23 -05:00
|
|
|
|
2023-06-28 06:03:04 -05:00
|
|
|
# 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
|
|
|
|
|
2020-05-18 11:05:34 -05:00
|
|
|
private_class_method :reload_locale!
|
|
|
|
private_class_method :clear_cached_keys!
|
2018-03-16 15:46:49 -05:00
|
|
|
private_class_method :i18n_changed
|
|
|
|
private_class_method :expire_cache
|
2017-11-20 02:10:06 -06:00
|
|
|
|
2023-07-14 03:52:39 -05:00
|
|
|
def original_translation_deleted?
|
|
|
|
!I18n.overrides_disabled { I18n.t!(transformed_key, locale: :en) }.is_a?(String)
|
|
|
|
rescue I18n::MissingTranslationData
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
2023-07-09 21:06:40 -05:00
|
|
|
def original_translation_updated?
|
|
|
|
return false if original_translation.blank?
|
2017-11-20 02:10:06 -06:00
|
|
|
|
2023-07-19 10:06:13 -05:00
|
|
|
original_translation != current_default
|
2023-07-09 21:06:40 -05:00
|
|
|
end
|
|
|
|
|
|
|
|
def invalid_interpolation_keys
|
2023-07-19 10:06:13 -05:00
|
|
|
return [] if current_default.blank?
|
2023-07-09 21:06:40 -05:00
|
|
|
|
2023-07-19 10:06:13 -05:00
|
|
|
original_interpolation_keys = I18nInterpolationKeysFinder.find(current_default)
|
2023-07-09 21:06:40 -05:00
|
|
|
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) }
|
2017-06-15 03:08:23 -05:00
|
|
|
end
|
2023-07-09 21:06:40 -05:00
|
|
|
|
|
|
|
(original_interpolation_keys | new_interpolation_keys) - original_interpolation_keys -
|
|
|
|
custom_interpolation_keys
|
|
|
|
end
|
|
|
|
|
2023-07-19 10:06:13 -05:00
|
|
|
def current_default
|
|
|
|
I18n.overrides_disabled { I18n.t(transformed_key, locale: :en) }
|
|
|
|
end
|
|
|
|
|
2023-07-09 21:06:40 -05:00
|
|
|
private
|
|
|
|
|
2023-07-14 03:52:39 -05:00
|
|
|
def transformed_key
|
|
|
|
@transformed_key ||= self.class.transform_pluralized_key(translation_key)
|
|
|
|
end
|
|
|
|
|
2023-07-09 21:06:40 -05:00
|
|
|
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,
|
|
|
|
),
|
|
|
|
)
|
2018-03-16 15:46:49 -05:00
|
|
|
end
|
2015-11-13 15:34:13 -06:00
|
|
|
end
|
2016-01-11 00:30:56 -06:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: translation_overrides
|
|
|
|
#
|
2023-07-09 21:06:40 -05:00
|
|
|
# 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
|
|
|
|
# compiled_js :text
|
|
|
|
# original_translation :text
|
|
|
|
# status :integer default("up_to_date"), not null
|
2016-01-11 00:30:56 -06:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
|
|
|
# index_translation_overrides_on_locale_and_translation_key (locale,translation_key) UNIQUE
|
|
|
|
#
|