Files
nairobi/app/models/concerns/globalizable.rb
Javi Martín 9841a9b03a Use in_order_of to sort translations by fallback
This method was introduced in Rails 7.0, and thanks to it we can
simplify the code that gets the translations in order.

We tried to use this method to simplify the `Randomizable` concern as
well. However, we found out that, when ordering tens of thousands of
records, the query could take several minutes, so we aren't using it in
this case. Using it for translation fallbacks is OK, since there's a
good chance we're never going to have tens of thousands of available
locales.

Note that automated security tools reported a false positive related to
SQL Injection due to the way we used `LEFT JOIN`, so now we get one less
false positive in these reports.
2024-07-22 17:50:40 +02:00

115 lines
3.7 KiB
Ruby

module Globalizable
MIN_TRANSLATIONS = 1
extend ActiveSupport::Concern
included do
globalize_accessors
accepts_nested_attributes_for :translations, allow_destroy: true
validate :check_translations_number, on: :update, if: :translations_required?
after_validation :copy_error_to_current_translation, on: :update
def locales_not_marked_for_destruction
translations.reject(&:marked_for_destruction?).map(&:locale)
end
def locales_persisted_and_marked_for_destruction
translations.select { |t| t.persisted? && t.marked_for_destruction? }.map(&:locale)
end
def translations_required?
translated_attribute_names.any? { |attr| required_attribute?(attr) }
end
if paranoid? && translation_class.attribute_names.include?("hidden_at")
translation_class.send :acts_as_paranoid, column: :hidden_at
end
private
def required_attribute?(attribute)
self.class.validators_on(attribute).any? do |validator|
validator.kind == :presence && !conditional_validator?(validator)
end
end
def conditional_validator?(validator)
return false unless validator.options[:unless]
if validator.options[:unless].to_proc.arity.zero?
instance_exec(&validator.options[:unless])
else
validator.options[:unless].to_proc.call(self)
end
end
def check_translations_number
errors.add(:base, :translations_too_short) unless traslations_count_valid?
end
def traslations_count_valid?
translations.reject(&:marked_for_destruction?).count >= MIN_TRANSLATIONS
end
def copy_error_to_current_translation
return unless errors.added?(:base, :translations_too_short)
if locales_persisted_and_marked_for_destruction.include?(I18n.locale)
locale = I18n.locale
else
locale = locales_persisted_and_marked_for_destruction.first
end
translation = translation_for(locale)
translation.errors.add(:base, :translations_too_short)
end
def searchable_globalized_values
values = {}
translations.each do |translation|
Globalize.with_locale(translation.locale) do
values.merge! searchable_translations_definitions
end
end
values
end
end
class_methods do
def validates_translation(method, options = {})
validates(method, options.merge(if: lambda { |resource| resource.translations.blank? }))
if options.include?(:length)
translation_class.instance_eval do
validates method,
length: options[:length],
if: lambda { |translation| translation.locale == Setting.default_locale }
end
if options.count > 1
translation_class.instance_eval do
validates method, options.reject { |key| key == :length }
end
end
else
translation_class.instance_eval { validates method, options }
end
end
def translation_class_delegate(method)
translation_class.instance_eval { delegate method, to: :globalized_model }
end
def with_fallback_translation
translations_foreign_key = reflect_on_association(:translations).foreign_key
fallbacks = Globalize.fallbacks(Globalize.locale)
translations_ids = translation_class
.select("DISTINCT ON (#{translations_foreign_key}) id")
.where(locale: fallbacks)
.order(translations_foreign_key)
.in_order_of(:locale, fallbacks)
with_translations(fallbacks).where("#{translations_table_name}.id": translations_ids)
end
end
end