Aggiunge traduzioni it/en/fr/de/es agli avvisi e toglie gli override Android/iOS.
Un avviso ha un testo per lingua; per differenziare le piattaforme si creano due avvisi. L'app riceve già il copy risolto da Accept-Language. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -8,7 +8,18 @@ class AppAnnouncement < ApplicationRecord
|
||||
web_public: :show_on_web_public,
|
||||
web_private: :show_on_web_private
|
||||
}.freeze
|
||||
URL_FIELDS = %i[action_url android_action_url ios_action_url].freeze
|
||||
COPY_KEYS = %w[title body action_url action_label].freeze
|
||||
URL_KEYS = %w[action_url].freeze
|
||||
TITLE_KEYS = %w[title].freeze
|
||||
BODY_KEYS = %w[body].freeze
|
||||
LABEL_KEYS = %w[action_label].freeze
|
||||
LOCALE_FALLBACKS = {
|
||||
"it" => %w[it],
|
||||
"en" => %w[en it],
|
||||
"fr" => %w[fr en it],
|
||||
"de" => %w[de en it],
|
||||
"es" => %w[es en it]
|
||||
}.freeze
|
||||
|
||||
belongs_to :created_by_admin, class_name: "AdminAccount", optional: true
|
||||
|
||||
@@ -16,14 +27,16 @@ class AppAnnouncement < ApplicationRecord
|
||||
validates :kind, inclusion: { in: KINDS }
|
||||
validates :severity, inclusion: { in: SEVERITIES }
|
||||
validates :status, inclusion: { in: STATUSES }
|
||||
validates :title, :android_title, :ios_title, length: { maximum: 120 }, allow_blank: true
|
||||
validates :body, :android_body, :ios_body, length: { maximum: 2000 }, allow_blank: true
|
||||
validates :action_label, :android_action_label, :ios_action_label, length: { maximum: 40 }, allow_blank: true
|
||||
URL_FIELDS.each do |field|
|
||||
validates :title, length: { maximum: 120 }, allow_blank: true
|
||||
validates :body, length: { maximum: 2000 }, allow_blank: true
|
||||
validates :action_label, length: { maximum: 40 }, allow_blank: true
|
||||
URL_KEYS.each do |field|
|
||||
validates field, format: { with: /\Ahttps?:\/\/.+/i, allow_blank: true }
|
||||
end
|
||||
validate :ends_at_after_starts_at
|
||||
validate :at_least_one_channel
|
||||
validate :translations_are_valid
|
||||
before_validation :normalize_translations
|
||||
|
||||
scope :newest, -> { order(created_at: :desc) }
|
||||
scope :published, -> { where(status: "published") }
|
||||
@@ -63,25 +76,49 @@ class AppAnnouncement < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def as_api_json(platform: nil)
|
||||
def copy_values_for(locale)
|
||||
loc = locale.to_s
|
||||
if default_locale?(loc)
|
||||
COPY_KEYS.index_with { |key| public_send(key).to_s.presence }
|
||||
else
|
||||
translation_hash(loc)
|
||||
end
|
||||
end
|
||||
|
||||
def filled_locale_codes
|
||||
LocaleResolver.available.select { |loc| locale_has_copy?(loc) }.map(&:to_s)
|
||||
end
|
||||
|
||||
def as_api_json(platform: nil, locale: I18n.locale)
|
||||
{
|
||||
id: id,
|
||||
kind: kind,
|
||||
severity: severity,
|
||||
title: copy_for(platform, :title),
|
||||
body: copy_for(platform, :body),
|
||||
title: copy_for("title", locale: locale),
|
||||
body: copy_for("body", locale: locale),
|
||||
dismissible: dismissible,
|
||||
action_url: resolved_action_url(platform),
|
||||
action_label: copy_for(platform, :action_label).presence,
|
||||
action_url: resolved_action_url(platform, locale: locale),
|
||||
action_label: copy_for("action_label", locale: locale).presence,
|
||||
starts_at: starts_at&.iso8601,
|
||||
ends_at: ends_at&.iso8601
|
||||
}
|
||||
end
|
||||
|
||||
def resolved_action_url(platform)
|
||||
override = platform_override(platform, :action_url)
|
||||
return override if override.present?
|
||||
return action_url if action_url.present?
|
||||
def localized_title(locale: I18n.locale)
|
||||
copy_for("title", locale: locale)
|
||||
end
|
||||
|
||||
def localized_body(locale: I18n.locale)
|
||||
copy_for("body", locale: locale)
|
||||
end
|
||||
|
||||
def localized_action_label(locale: I18n.locale)
|
||||
copy_for("action_label", locale: locale)
|
||||
end
|
||||
|
||||
def resolved_action_url(platform, locale: I18n.locale)
|
||||
url = copy_for("action_url", locale: locale)
|
||||
return url if url.present?
|
||||
return nil unless kind == "update"
|
||||
|
||||
case platform.to_s
|
||||
@@ -92,21 +129,73 @@ class AppAnnouncement < ApplicationRecord
|
||||
|
||||
private
|
||||
|
||||
def copy_for(platform, field)
|
||||
override = platform_override(platform, field)
|
||||
override.presence || public_send(field)
|
||||
def copy_for(field, locale: I18n.locale)
|
||||
field = field.to_s
|
||||
locale_chain(locale).each do |loc|
|
||||
value = value_for(loc, field)
|
||||
return value if value.present?
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def platform_override(platform, field)
|
||||
prefix = case platform.to_s
|
||||
when "android" then :android
|
||||
when "ios" then :ios
|
||||
end
|
||||
return if prefix.blank?
|
||||
|
||||
public_send("#{prefix}_#{field}")
|
||||
def value_for(locale, key)
|
||||
copy_values_for(locale)[key].presence
|
||||
end
|
||||
|
||||
def translation_hash(locale)
|
||||
raw = translations.is_a?(Hash) ? translations : {}
|
||||
values = raw.stringify_keys[locale.to_s]
|
||||
return {} unless values.is_a?(Hash)
|
||||
|
||||
values.stringify_keys
|
||||
end
|
||||
|
||||
def locale_chain(locale)
|
||||
loc = (LocaleResolver.normalize(locale) || I18n.default_locale).to_s
|
||||
LOCALE_FALLBACKS[loc] || [loc, I18n.default_locale.to_s].uniq
|
||||
end
|
||||
|
||||
def locale_has_copy?(locale)
|
||||
values = copy_values_for(locale)
|
||||
values["title"].present? || values["body"].present?
|
||||
end
|
||||
|
||||
def default_locale?(locale)
|
||||
locale.to_s == I18n.default_locale.to_s
|
||||
end
|
||||
|
||||
def normalize_translations
|
||||
self.translations = self.class.sanitize_translations(translations)
|
||||
end
|
||||
|
||||
def self.sanitize_translations(value)
|
||||
raw = coerce_translation_hash(value)
|
||||
LocaleResolver.available.each_with_object({}) do |loc, acc|
|
||||
next if loc.to_s == I18n.default_locale.to_s
|
||||
|
||||
payload = raw[loc.to_s]
|
||||
next unless payload.is_a?(Hash)
|
||||
|
||||
cleaned = COPY_KEYS.each_with_object({}) do |key, fields|
|
||||
text = payload[key].to_s.strip
|
||||
fields[key] = text if text.present?
|
||||
end
|
||||
acc[loc.to_s] = cleaned if cleaned.any?
|
||||
end
|
||||
end
|
||||
|
||||
def self.coerce_translation_hash(value)
|
||||
hash = if value.is_a?(ActionController::Parameters)
|
||||
value.to_unsafe_h
|
||||
elsif value.is_a?(Hash)
|
||||
value
|
||||
else
|
||||
{}
|
||||
end
|
||||
hash.deep_stringify_keys
|
||||
end
|
||||
private_class_method :coerce_translation_hash
|
||||
|
||||
def at_least_one_channel
|
||||
return if show_on_android? || show_on_ios? || show_on_web_public? || show_on_web_private?
|
||||
|
||||
@@ -119,4 +208,33 @@ class AppAnnouncement < ApplicationRecord
|
||||
|
||||
errors.add(:ends_at, :invalid)
|
||||
end
|
||||
|
||||
def translations_are_valid
|
||||
translation_entries.each do |locale, payload|
|
||||
TITLE_KEYS.each do |key|
|
||||
errors.add(:base, :translation_too_long, locale: locale, field: key) if payload[key].to_s.length > 120
|
||||
end
|
||||
BODY_KEYS.each do |key|
|
||||
errors.add(:base, :translation_too_long, locale: locale, field: key) if payload[key].to_s.length > 2000
|
||||
end
|
||||
LABEL_KEYS.each do |key|
|
||||
errors.add(:base, :translation_too_long, locale: locale, field: key) if payload[key].to_s.length > 40
|
||||
end
|
||||
URL_KEYS.each do |key|
|
||||
url = payload[key].to_s
|
||||
next if url.blank? || url.match?(/\Ahttps?:\/\/.+/i)
|
||||
|
||||
errors.add(:base, :translation_invalid_url, locale: locale)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def translation_entries
|
||||
raw = translations.is_a?(Hash) ? translations : {}
|
||||
raw.stringify_keys.filter_map do |locale, payload|
|
||||
next unless payload.is_a?(Hash)
|
||||
|
||||
[locale, payload.stringify_keys]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user