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)

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 Models::EventPublishable

#publish_event

Instance Attribute Details

#auto_translate_missingObject

Returns the value of attribute auto_translate_missing.



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

def auto_translate_missing
  @auto_translate_missing
end

#keyObject (readonly)



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

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

Class Method Details

.available_localesObject



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

def self.available_locales
  EDITABLE_TRANSLATIONS # Symbols
end

.newline_normalize(key) ⇒ Object



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

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

.normalize_locale(locale) ⇒ Object



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

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)


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

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



73
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
# File 'app/models/translation_key.rb', line 73

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



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

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



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

def auto_translate(force: false, locales: nil)
  english_text = translations.detect { |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.detect { |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



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

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 }.each_with_object({}) { |t, hsh| hsh[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.detect { |t| t.locale == 'en' }&.destroy
  end
  translations.reload
end

#translation_key_resourcesActiveRecord::Relation<TranslationKeyResource>

Returns:

See Also:



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

has_many :translation_key_resources, dependent: :destroy

#translationsActiveRecord::Relation<TranslationText>

Returns:

See Also:



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

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

#update_dependents(locales: nil) ⇒ Object



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

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