translation.rb 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. # Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. class Translation < ApplicationModel
  3. include Translation::SynchronizesFromPo
  4. before_create :set_initial
  5. validates :locale, presence: true
  6. scope :sources, -> { where(locale: 'en-us', is_synchronized_from_codebase: true) }
  7. scope :details, -> { select(:id, :locale, :source, :target, :target_initial, :is_synchronized_from_codebase) }
  8. scope :customized, -> { where('target_initial != target OR is_synchronized_from_codebase = false').reorder(locale: :asc, source: :asc) }
  9. scope :not_customized, -> { where('target_initial = target AND is_synchronized_from_codebase = true').reorder(source: :asc) }
  10. =begin
  11. reset translations to origin
  12. Translation.reset(locale)
  13. =end
  14. def self.reset(locale)
  15. # only push changed translations
  16. translations = Translation.where(locale: locale)
  17. translations.each do |translation|
  18. if translation.target_initial.blank?
  19. translation.destroy
  20. elsif translation.target != translation.target_initial
  21. translation.target = translation.target_initial
  22. translation.save
  23. end
  24. end
  25. true
  26. end
  27. =begin
  28. get list of translations
  29. list = Translation.lang('de-de')
  30. =end
  31. def self.lang(locale)
  32. locale = locale.downcase
  33. Rails.cache.fetch("#{self}/#{latest_change}/lang/#{locale}") do
  34. list = Translation
  35. .where(locale: locale).where.not(target: '')
  36. .reorder(:source)
  37. .map do |item|
  38. [
  39. item.id,
  40. item.source,
  41. item.target,
  42. ]
  43. end
  44. {
  45. 'total' => Translation.where(locale: locale).count,
  46. 'list' => list
  47. }
  48. end
  49. end
  50. =begin
  51. translate strings in Ruby context, e. g. for notifications
  52. translated = Translation.translate('de-de', 'New')
  53. =end
  54. def self.translate(locale, string, *args)
  55. translated = find_source(locale, string)&.target.presence || string
  56. translated %= args if args.any?
  57. translated
  58. end
  59. =begin
  60. translate multiple strings at the same time
  61. translated = Translation.translate_all('de-de', 'closed', 'open', 'new')
  62. =end
  63. # Translate multiple strings in bulk
  64. #
  65. # @param locale [String] locale code
  66. # @param [*Array<String>] *args is the list of translations
  67. #
  68. # @return [Hash{String => String}]
  69. #
  70. # @example
  71. #
  72. # Translation.translate_all('de-de', 'closed', 'open', 'new') # {"closed"=>"geschlossen", "open"=>"offen", "new"=>"neu"}
  73. #
  74. def self.translate_all(locale, *args)
  75. sources = args.map { |arg| arg.is_a?(Array) ? arg[0] : arg }
  76. translations = where(locale: locale, source: sources)
  77. .pluck(:source, :target)
  78. .each_with_object({}) do |(source, target), hash|
  79. hash[source] = target
  80. end
  81. args.each_with_object({}) do |arg, memo|
  82. (source, placeholders) = arg.is_a?(Array) ? [arg[0], arg.drop(1)] : [arg]
  83. translation = translations[source].presence || source
  84. memo[source] = translation % placeholders
  85. end
  86. end
  87. =begin
  88. find a translation record for a given locale and source string. 'find_by' might not be enough,
  89. because it could produce wrong matches on case insensitive MySQL databases.
  90. =end
  91. def self.find_source(locale, string)
  92. if ActiveRecord::Base.connection_db_config.configuration_hash[:adapter] == 'mysql2'
  93. # MySQL might find the wrong record with find_by with case insensitive locales, so use a direct comparison.
  94. where(locale: locale, source: string).find { |t| t.source.eql? string }
  95. else
  96. find_by(locale: locale, source: string)
  97. end
  98. end
  99. =begin
  100. translate timestamps in ruby context, e. g. for notifications
  101. translated = Translation.timestamp('de-de', 'Europe/Berlin', '2018-10-10T10:00:00Z0')
  102. or
  103. translated = Translation.timestamp('de-de', 'Europe/Berlin', Time.zone.parse('2018-10-10T10:00:00Z0'))
  104. =end
  105. def self.timestamp(locale, timezone, timestamp, append_timezone: true)
  106. if timestamp.instance_of?(String)
  107. begin
  108. timestamp_parsed = Time.zone.parse(timestamp)
  109. return timestamp.to_s if !timestamp_parsed
  110. timestamp = timestamp_parsed
  111. rescue
  112. return timestamp.to_s
  113. end
  114. end
  115. begin
  116. timestamp = timestamp.in_time_zone(timezone)
  117. rescue
  118. return timestamp.to_s
  119. end
  120. record = Translation.where(locale: locale, source: 'FORMAT_DATETIME').pick(:target)
  121. return timestamp.to_s if !record
  122. record.sub!('dd', format('%<day>02d', day: timestamp.day))
  123. record.sub!('d', timestamp.day.to_s)
  124. record.sub!('mm', format('%<month>02d', month: timestamp.month))
  125. record.sub!('m', timestamp.month.to_s)
  126. record.sub!('yyyy', timestamp.year.to_s)
  127. record.sub!('yy', timestamp.year.to_s.last(2))
  128. record.sub!('SS', format('%<second>02d', second: timestamp.sec.to_s))
  129. record.sub!('MM', format('%<minute>02d', minute: timestamp.min.to_s))
  130. record.sub!('HH', format('%<hour>02d', hour: timestamp.hour.to_s))
  131. record.sub!('l', timestamp.strftime('%l'))
  132. record.sub!('P', timestamp.strftime('%P'))
  133. record += " (#{timezone})" if append_timezone
  134. record
  135. end
  136. =begin
  137. translate date in ruby context, e. g. for notifications
  138. translated = Translation.date('de-de', '2018-10-10')
  139. or
  140. translated = Translation.date('de-de', Date.parse('2018-10-10'))
  141. =end
  142. def self.date(locale, date)
  143. if date.instance_of?(String)
  144. begin
  145. date_parsed = Date.parse(date)
  146. return date.to_s if !date_parsed
  147. date = date_parsed
  148. rescue
  149. return date.to_s
  150. end
  151. end
  152. return date.to_s if date.class != Date
  153. record = Translation.where(locale: locale, source: 'FORMAT_DATE').pick(:target)
  154. return date.to_s if !record
  155. record.sub!('dd', format('%<day>02d', day: date.day))
  156. record.sub!('d', date.day.to_s)
  157. record.sub!('mm', format('%<month>02d', month: date.month))
  158. record.sub!('m', date.month.to_s)
  159. record.sub!('yyyy', date.year.to_s)
  160. record.sub!('yy', date.year.to_s.last(2))
  161. record
  162. end
  163. def reset
  164. return if !is_synchronized_from_codebase || target_initial == target
  165. self.target = target_initial
  166. save!
  167. end
  168. private
  169. def set_initial
  170. return true if target_initial.present?
  171. return true if target_initial == ''
  172. self.target_initial = target
  173. true
  174. end
  175. end