Class: TranslationKey

Inherits:
ApplicationRecord show all
Defined in:
app/models/translation_key.rb

Overview

== Schema Information

Table name: translation_keys
Database name: primary

id :bigint not null, primary key
key :string not null
namespace :string
created_at :datetime not null
updated_at :datetime not null

Indexes

index_translation_keys_on_key (key) UNIQUE WHERE (namespace IS NULL)
index_translation_keys_on_namespace_and_key (namespace,key) UNIQUE WHERE (namespace IS NOT NULL)

Constant Summary

Constants included from Schedulable

Schedulable::SIMPLE_FORM_OPTIONS

Instance Attribute Summary collapse

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransackable_scopes, ransortable_attributes, #to_relation

Methods included from Schedulable

config

Methods included from Models::AfterCommittable

#after_commit

Methods included from Models::EventPublishable

#publish_event

Instance Attribute Details

#auto_translate_missingObject

Returns the value of attribute auto_translate_missing.



30
31
32
# File 'app/models/translation_key.rb', line 30

def auto_translate_missing
  @auto_translate_missing
end

#keyObject (readonly)



25
# File 'app/models/translation_key.rb', line 25

validates :key, uniqueness: { scope: :namespace }, presence: true

Class Method Details

.available_localesObject



53
54
55
# File 'app/models/translation_key.rb', line 53

def self.available_locales
  EDITABLE_TRANSLATIONS # Symbols
end

.newline_normalize(key) ⇒ Object



57
58
59
# File 'app/models/translation_key.rb', line 57

def self.newline_normalize(key)
  key&.to_s&.gsub("\r\n", "\n")&.squish.presence
end

.normalize_locale(locale) ⇒ Object



48
49
50
51
# File 'app/models/translation_key.rb', line 48

def self.normalize_locale(locale)
  lang, country_iso = locale.to_s.split(/[-_]/)
  [lang.downcase, country_iso&.upcase].compact.join('_')
end

.translatable?(text) ⇒ Boolean

Applies rules to determine if this string should even be part of our translation table, e.g avoid noise word

Returns:

  • (Boolean)


62
63
64
65
66
67
68
69
70
# File 'app/models/translation_key.rb', line 62

def self.translatable?(text)
  noise_words = %w[degC]
  # First, only 3 letters contiguous or more qualify
  return false unless text.match?(/[a-zA-Z]{3}/)
  # Second, noise words are disqualified
  return false if noise_words.include?(text)

  true
end

.translate(text, locale = I18n.locale, namespace: nil, auto_create: true, auto_translate: true, _key: nil, resource: nil, resource_attribute: nil) ⇒ Object

Translate will leverage fast get text to retrieve a translation but if it is missing, it will create it and queue it for
auto translation



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'app/models/translation_key.rb', line 74

def self.translate(text, locale = I18n.locale, namespace: nil, auto_create: true, auto_translate: true, _key: nil, resource: nil, resource_attribute: nil)
  text = newline_normalize(text)
  fg_locale = LocaleUtility.fastgettext_locale(locale).first(2)
  composite_key = [namespace, text].compact.join('|')
  translation = FastGettext.with_locale(fg_locale) do
    # Try first with a namespace key search if specified, but it not present, fallback to just the text
    t = s_(composite_key) { nil }
    t = _(text) { nil } if t.blank? || (t == text && namespace && fg_locale.to_s.first(2) != 'en')
    t
  end
  # If you're asking for a translation and get nothing, if we auto create we create the record
  translation_key = nil
  if translation.nil? && fg_locale.first(2) != 'en'
    if auto_create
      translation_key = where(key: text, namespace: namespace).first_or_create!
      if auto_translate
        translations = translation_key.auto_translate(locales: [locale])
        translation = translations[locale.to_s.first(2).to_sym]
      end
    elsif auto_translate # Translate in place
      translation = DeepL.translate(text, 'EN', fg_locale.first(2).upcase, tag_handling: 'html', ignore_tags: %w[code pre]).text
    end
  end
  # We have to latch this translation resource to its key if provided
  if resource && (translation_key ||= find_by(key: text, namespace: namespace))
    translation_key.translation_key_resources.where(resource_type: resource.class.name, resource_id: resource.id).first_or_create!(resource_attribute: resource_attribute)
  end

  translation
end

.translation(composite_key, locale) ⇒ Object

This is the method FastGettext will call, but it doesn't auto create keys necessarily, so for that
you can use the translate call which also does some fallbacks with namespaced keys



36
37
38
39
40
41
42
43
44
45
46
# File 'app/models/translation_key.rb', line 36

def self.translation(composite_key, locale)
  # Key might be scoped
  normalized_key = newline_normalize(composite_key)
  key, namespace = normalized_key.split('|').reverse
  query = TranslationText.joins(:translation_key).where(locale: locale, translation_key: { key: key })
  query = query.where(translation_key: { namespace: namespace }) if namespace.present?
  txt, last_accessed_at = query.pick(:text, :last_accessed_at)
  # For now we don't need to refresh this updated time every access, but we can do it once a day
  query.update_all(last_accessed_at: Time.current) if last_accessed_at.present? && last_accessed_at.to_date != Date.current
  txt
end

Instance Method Details

#auto_translate(force: false, locales: nil) ⇒ Object

Leverage Deepl and auto translate
force true means redo an existing translation



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'app/models/translation_key.rb', line 107

def auto_translate(force: false, locales: nil)
  english_text = translations.find { |t| t.locale.to_s.first(2) == 'en' }&.text || key
  result = {}
  locales ||= self.class.available_locales
  lang_codes = locales.map { |locale| locale.to_s.first(2) }.uniq - ['en']
  lang_codes.each do |lang_code|
    t = translations.find { |t| t.locale == lang_code.to_s } || translations.build(locale: lang_code)
    t.text = nil if force
    t.text ||= DeepL.translate(english_text, 'EN', lang_code.upcase, tag_handling: 'html', ignore_tags: %w[code pre]).text
    result[lang_code.to_sym] = t.text
    t.save
  end
  # And compact
  compact_translations
  result
end

#compact_translationsObject



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'app/models/translation_key.rb', line 124

def compact_translations
  TranslationText.transaction do
    # Load up the fallbacks first (those with 2 character locale only) in a hash
    core_translations_hsh = translations.to_a.select { |t| t.locale.to_s.size == 2 }.to_h { |t| [t.locale, t.text] }
    # the english core is the key
    core_translations_hsh['en'] ||= key
    # Now iterate through the localized transation
    translations.to_a.select { |t| t.locale.to_s.size > 2 }.each do |translation|
      # If our translation for this locale is the same as the core language translation fallback, we can discard it
      _, locale_fallback = I18n.fallbacks[translation.locale.to_sym]
      translation.destroy if core_translations_hsh[locale_fallback.to_s] == translation.text
    end
    # Destroy the english!
    translations.find { |t| t.locale == 'en' }&.destroy
  end
  translations.reload
end

#translation_key_resourcesActiveRecord::Relation<TranslationKeyResource>

Returns:

See Also:



20
# File 'app/models/translation_key.rb', line 20

has_many :translation_key_resources, dependent: :destroy

#translationsActiveRecord::Relation<TranslationText>

Returns:

See Also:



19
# File 'app/models/translation_key.rb', line 19

has_many :translations, class_name: 'TranslationText', dependent: :destroy

#update_dependents(locales: nil) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'app/models/translation_key.rb', line 142

def update_dependents(locales: nil)
  translations_rel = translations
  translations_rel = translations_rel.where(locale: locales) if locales.present?
  translations_rel.each do |translation_text|
    translation_key_resources.each do |tkr|
      attr = :"#{tkr.resource_attribute}_#{translation_text.locale}"
      if tkr.resource.respond_to?(attr)
        if tkr.resource.send(attr) != translation_text.text
          tkr.resource.send(:"#{attr}=", translation_text.text)
          tkr.resource.save
        end
      else
        Rails.logger.error "No attribute #{attr} in resource #{resource.class.name} #{resource}"
      end
    end
  end
end