translation.rb 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
  2. require 'csv'
  3. class Translation < ApplicationModel
  4. before_create :set_initial
  5. after_create :cache_clear
  6. after_update :cache_clear
  7. after_destroy :cache_clear
  8. =begin
  9. sync translations from local if exists, otherwise from online
  10. all:
  11. Translation.sync
  12. Translation.sync(locale) # e. g. 'en-us' or 'de-de'
  13. =end
  14. def self.sync(dedicated_locale = nil)
  15. return true if load_from_file(dedicated_locale)
  16. load
  17. end
  18. =begin
  19. load translations from online
  20. all:
  21. Translation.load
  22. dedicated:
  23. Translation.load(locale) # e. g. 'en-us' or 'de-de'
  24. =end
  25. def self.load(dedicated_locale = nil)
  26. locals_to_sync(dedicated_locale).each do |locale|
  27. fetch(locale)
  28. load_from_file(locale)
  29. end
  30. true
  31. end
  32. =begin
  33. push translations to online
  34. Translation.push(locale)
  35. =end
  36. def self.push(locale)
  37. # only push changed translations
  38. translations = Translation.where(locale: locale)
  39. translations_to_push = []
  40. translations.each do |translation|
  41. if translation.target != translation.target_initial
  42. translations_to_push.push translation
  43. end
  44. end
  45. return true if translations_to_push.blank?
  46. url = 'https://i18n.zammad.com/api/v1/translations/thanks_for_your_support'
  47. translator_key = Setting.get('translator_key')
  48. result = UserAgent.post(
  49. url,
  50. {
  51. locale: locale,
  52. translations: translations_to_push,
  53. fqdn: Setting.get('fqdn'),
  54. translator_key: translator_key,
  55. },
  56. {
  57. json: true,
  58. open_timeout: 8,
  59. read_timeout: 24,
  60. }
  61. )
  62. raise "Can't push translations to #{url}: #{result.error}" if !result.success?
  63. # set new translator_key if given
  64. if result.data['translator_key']
  65. translator_key = Setting.set('translator_key', result.data['translator_key'])
  66. end
  67. true
  68. end
  69. =begin
  70. reset translations to origin
  71. Translation.reset(locale)
  72. =end
  73. def self.reset(locale)
  74. # only push changed translations
  75. translations = Translation.where(locale: locale)
  76. translations.each do |translation|
  77. if translation.target_initial.blank?
  78. translation.destroy
  79. elsif translation.target != translation.target_initial
  80. translation.target = translation.target_initial
  81. translation.save
  82. end
  83. end
  84. true
  85. end
  86. =begin
  87. get list of translations
  88. list = Translation.lang('de-de')
  89. =end
  90. def self.lang(locale, admin = false)
  91. # use cache if not admin page is requested
  92. if !admin
  93. data = cache_get(locale)
  94. return data if data
  95. end
  96. # show total translations as reference count
  97. data = {
  98. 'total' => Translation.where(locale: 'de-de').count,
  99. }
  100. list = []
  101. translations = if admin
  102. Translation.where(locale: locale.downcase).order(:source)
  103. else
  104. Translation.where(locale: locale.downcase).where.not(target: '').order(:source)
  105. end
  106. translations.each do |item|
  107. translation_item = []
  108. translation_item = if admin
  109. [
  110. item.id,
  111. item.source,
  112. item.target,
  113. item.target_initial,
  114. item.format,
  115. ]
  116. else
  117. [
  118. item.id,
  119. item.source,
  120. item.target,
  121. item.format,
  122. ]
  123. end
  124. list.push translation_item
  125. end
  126. # add presorted on top
  127. presorted_list = []
  128. %w[yes no or Year Years Month Months Day Days Hour Hours Minute Minutes Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec January February March April May June July August September October November December Mon Tue Wed Thu Fri Sat Sun Monday Tuesday Wednesday Thursday Friday Saturday Sunday].each do |presort|
  129. list.each do |item|
  130. next if item[1] != presort
  131. presorted_list.push item
  132. list.delete item
  133. #list.unshift presort
  134. end
  135. end
  136. data['list'] = presorted_list.concat list
  137. # set cache
  138. if !admin
  139. cache_set(locale, data)
  140. end
  141. data
  142. end
  143. =begin
  144. translate strings in ruby context, e. g. for notifications
  145. translated = Translation.translate('de-de', 'New')
  146. =end
  147. def self.translate(locale, string)
  148. # translate string
  149. records = Translation.where(locale: locale, source: string)
  150. records.each do |record|
  151. return record.target if record.source == string
  152. end
  153. # fallback lookup in en
  154. records = Translation.where(locale: 'en', source: string)
  155. records.each do |record|
  156. return record.target if record.source == string
  157. end
  158. string
  159. end
  160. =begin
  161. load translations from local
  162. all:
  163. Translation.load_from_file
  164. or
  165. Translation.load_from_file(locale) # e. g. 'en-us' or 'de-de'
  166. =end
  167. def self.load_from_file(dedicated_locale = nil)
  168. version = Version.get
  169. directory = Rails.root.join('config', 'translations')
  170. locals_to_sync(dedicated_locale).each do |locale|
  171. file = Rails.root.join(directory, "#{locale}-#{version}.yml")
  172. return false if !File.exist?(file)
  173. data = YAML.load_file(file)
  174. to_database(locale, data)
  175. end
  176. true
  177. end
  178. =begin
  179. fetch translation from remote and store them in local file system
  180. all:
  181. Translation.fetch
  182. or
  183. Translation.fetch(locale) # e. g. 'en-us' or 'de-de'
  184. =end
  185. def self.fetch(dedicated_locale = nil)
  186. version = Version.get
  187. locals_to_sync(dedicated_locale).each do |locale|
  188. url = "https://i18n.zammad.com/api/v1/translations/#{locale}"
  189. if !UserInfo.current_user_id
  190. UserInfo.current_user_id = 1
  191. end
  192. result = UserAgent.get(
  193. url,
  194. {
  195. version: version,
  196. },
  197. {
  198. json: true,
  199. open_timeout: 8,
  200. read_timeout: 24,
  201. }
  202. )
  203. raise "Can't load translations from #{url}: #{result.error}" if !result.success?
  204. directory = Rails.root.join('config', 'translations')
  205. if !File.directory?(directory)
  206. Dir.mkdir(directory, 0o755)
  207. end
  208. file = Rails.root.join(directory, "#{locale}-#{version}.yml")
  209. File.open(file, 'w') do |out|
  210. YAML.dump(result.data, out)
  211. end
  212. end
  213. true
  214. end
  215. =begin
  216. load translations from csv file
  217. all:
  218. Translation.load_from_csv
  219. or
  220. Translation.load_from_csv(locale, file_location, file_charset) # e. g. 'en-us' or 'de-de' and /path/to/translation_list.csv
  221. e. g.
  222. Translation.load_from_csv('he-il', '/Users/me/Downloads/Hebrew_translation_list-1.csv', 'Windows-1255')
  223. Get source file at https://i18n.zammad.com/api/v1/translations_empty_translation_list
  224. =end
  225. def self.load_from_csv(locale_name, location, charset = 'UTF8')
  226. locale = Locale.find_by(locale: locale_name)
  227. if !locale
  228. raise "No such locale: #{locale_name}"
  229. end
  230. if !::File.exist?(location)
  231. raise "No such file: #{location}"
  232. end
  233. content = ::File.open(location, "r:#{charset}").read
  234. params = {
  235. col_sep: ',',
  236. }
  237. rows = ::CSV.parse(content, params)
  238. header = rows.shift
  239. translation_raw = []
  240. rows.each do |row|
  241. raise "Can't import translation, source is missing" if row[0].blank?
  242. if row[1].blank?
  243. warn "Skipped #{row[0]}, because translation is blank"
  244. next
  245. end
  246. raise "Can't import translation, format is missing" if row[2].blank?
  247. raise "Can't import translation, format is invalid (#{row[2]})" if row[2] !~ /^(time|string)$/
  248. item = {
  249. 'locale' => locale.locale,
  250. 'source' => row[0],
  251. 'target' => row[1],
  252. 'target_initial' => '',
  253. 'format' => row[2],
  254. }
  255. translation_raw.push item
  256. end
  257. to_database(locale.name, translation_raw)
  258. true
  259. end
  260. def self.remote_translation_need_update?(raw, translations)
  261. translations.each do |row|
  262. next if row[1] != raw['locale']
  263. next if row[2] != raw['source']
  264. next if row[3] != raw['format']
  265. return false if row[4] == raw['target'] # no update if target is still the same
  266. return false if row[4] != row[5] # no update if translation has already changed
  267. return [true, Translation.find(row[0])]
  268. end
  269. [true, nil]
  270. end
  271. private_class_method def self.to_database(locale, data)
  272. translations = Translation.where(locale: locale).pluck(:id, :locale, :source, :format, :target, :target_initial).to_a
  273. ActiveRecord::Base.transaction do
  274. translations_to_import = []
  275. data.each do |translation_raw|
  276. result = Translation.remote_translation_need_update?(translation_raw, translations)
  277. next if result == false
  278. next if result.class != Array
  279. if result[1]
  280. result[1].update!(translation_raw.symbolize_keys!)
  281. result[1].save
  282. else
  283. translation_raw['updated_by_id'] = UserInfo.current_user_id || 1
  284. translation_raw['created_by_id'] = UserInfo.current_user_id || 1
  285. translations_to_import.push Translation.new(translation_raw.symbolize_keys!)
  286. end
  287. end
  288. if translations_to_import.present?
  289. Translation.import translations_to_import
  290. end
  291. end
  292. end
  293. private_class_method def self.locals_to_sync(dedicated_locale = nil)
  294. locales_list = []
  295. if !dedicated_locale
  296. locales = Locale.to_sync
  297. locales.each do |locale|
  298. locales_list.push locale.locale
  299. end
  300. else
  301. locales_list = [dedicated_locale]
  302. end
  303. locales_list
  304. end
  305. private
  306. def set_initial
  307. return true if target_initial.present?
  308. return true if target_initial == ''
  309. self.target_initial = target
  310. true
  311. end
  312. def cache_clear
  313. Cache.delete('TranslationMapOnlyContent::' + locale.downcase)
  314. true
  315. end
  316. def self.cache_set(locale, data)
  317. Cache.write('TranslationMapOnlyContent::' + locale.downcase, data)
  318. end
  319. private_class_method :cache_set
  320. def self.cache_get(locale)
  321. Cache.get('TranslationMapOnlyContent::' + locale.downcase)
  322. end
  323. private_class_method :cache_get
  324. end