translation.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511
  1. # Copyright (C) 2012-2021 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. verify_ssl: true,
  61. }
  62. )
  63. raise "Can't push translations to #{url}: #{result.error}" if !result.success?
  64. # set new translator_key if given
  65. if result.data['translator_key']
  66. Setting.set('translator_key', result.data['translator_key'])
  67. end
  68. true
  69. end
  70. =begin
  71. reset translations to origin
  72. Translation.reset(locale)
  73. =end
  74. def self.reset(locale)
  75. # only push changed translations
  76. translations = Translation.where(locale: locale)
  77. translations.each do |translation|
  78. if translation.target_initial.blank?
  79. translation.destroy
  80. elsif translation.target != translation.target_initial
  81. translation.target = translation.target_initial
  82. translation.save
  83. end
  84. end
  85. true
  86. end
  87. =begin
  88. get list of translations
  89. list = Translation.lang('de-de')
  90. =end
  91. def self.lang(locale, admin = false)
  92. # use cache if not admin page is requested
  93. if !admin
  94. data = cache_get(locale)
  95. return data if data
  96. end
  97. # show total translations as reference count
  98. data = {
  99. 'total' => Translation.where(locale: 'de-de').count,
  100. }
  101. list = []
  102. translations = if admin
  103. Translation.where(locale: locale.downcase).order(:source)
  104. else
  105. Translation.where(locale: locale.downcase).where.not(target: '').order(:source)
  106. end
  107. translations.each do |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. translate timestampes in ruby context, e. g. for notifications
  162. translated = Translation.timestamp('de-de', 'Europe/Berlin', '2018-10-10T10:00:00Z0')
  163. or
  164. translated = Translation.timestamp('de-de', 'Europe/Berlin', Time.zone.parse('2018-10-10T10:00:00Z0'))
  165. =end
  166. def self.timestamp(locale, timezone, timestamp)
  167. if timestamp.instance_of?(String)
  168. begin
  169. timestamp_parsed = Time.zone.parse(timestamp)
  170. return timestamp.to_s if !timestamp_parsed
  171. timestamp = timestamp_parsed
  172. rescue
  173. return timestamp.to_s
  174. end
  175. end
  176. begin
  177. timestamp = timestamp.in_time_zone(timezone)
  178. rescue
  179. return timestamp.to_s
  180. end
  181. record = Translation.where(locale: locale, source: 'timestamp', format: 'time').pick(:target)
  182. return timestamp.to_s if !record
  183. record.sub!('dd', format('%<day>02d', day: timestamp.day))
  184. record.sub!('d', timestamp.day.to_s)
  185. record.sub!('mm', format('%<month>02d', month: timestamp.month))
  186. record.sub!('m', timestamp.month.to_s)
  187. record.sub!('yyyy', timestamp.year.to_s)
  188. record.sub!('yy', timestamp.year.to_s.last(2))
  189. record.sub!('SS', format('%<second>02d', second: timestamp.sec.to_s))
  190. record.sub!('MM', format('%<minute>02d', minute: timestamp.min.to_s))
  191. record.sub!('HH', format('%<hour>02d', hour: timestamp.hour.to_s))
  192. "#{record} (#{timezone})"
  193. end
  194. =begin
  195. translate date in ruby context, e. g. for notifications
  196. translated = Translation.date('de-de', '2018-10-10')
  197. or
  198. translated = Translation.date('de-de', Date.parse('2018-10-10'))
  199. =end
  200. def self.date(locale, date)
  201. if date.instance_of?(String)
  202. begin
  203. date_parsed = Date.parse(date)
  204. return date.to_s if !date_parsed
  205. date = date_parsed
  206. rescue
  207. return date.to_s
  208. end
  209. end
  210. return date.to_s if date.class != Date
  211. record = Translation.where(locale: locale, source: 'date', format: 'time').pick(:target)
  212. return date.to_s if !record
  213. record.sub!('dd', format('%<day>02d', day: date.day))
  214. record.sub!('d', date.day.to_s)
  215. record.sub!('mm', format('%<month>02d', month: date.month))
  216. record.sub!('m', date.month.to_s)
  217. record.sub!('yyyy', date.year.to_s)
  218. record.sub!('yy', date.year.to_s.last(2))
  219. record
  220. end
  221. =begin
  222. load translations from local
  223. all:
  224. Translation.load_from_file
  225. or
  226. Translation.load_from_file(locale) # e. g. 'en-us' or 'de-de'
  227. =end
  228. def self.load_from_file(dedicated_locale = nil)
  229. version = Version.get
  230. directory = Rails.root.join('config/translations')
  231. locals_to_sync(dedicated_locale).each do |locale|
  232. file = Rails.root.join(directory, "#{locale}-#{version}.yml")
  233. return false if !File.exist?(file)
  234. data = YAML.load_file(file)
  235. to_database(locale, data)
  236. end
  237. true
  238. end
  239. =begin
  240. fetch translation from remote and store them in local file system
  241. all:
  242. Translation.fetch
  243. or
  244. Translation.fetch(locale) # e. g. 'en-us' or 'de-de'
  245. =end
  246. def self.fetch(dedicated_locale = nil)
  247. version = Version.get
  248. locals_to_sync(dedicated_locale).each do |locale|
  249. url = "https://i18n.zammad.com/api/v1/translations/#{locale}"
  250. if !UserInfo.current_user_id
  251. UserInfo.current_user_id = 1
  252. end
  253. result = UserAgent.get(
  254. url,
  255. {
  256. version: version,
  257. },
  258. {
  259. json: true,
  260. open_timeout: 8,
  261. read_timeout: 24,
  262. verify_ssl: true,
  263. }
  264. )
  265. raise "Can't load translations from #{url}: #{result.error}" if !result.success?
  266. directory = Rails.root.join('config/translations')
  267. if !File.directory?(directory)
  268. Dir.mkdir(directory, 0o755)
  269. end
  270. file = Rails.root.join(directory, "#{locale}-#{version}.yml")
  271. File.open(file, 'w') do |out|
  272. YAML.dump(result.data, out)
  273. end
  274. end
  275. true
  276. end
  277. =begin
  278. load translations from csv file
  279. all:
  280. Translation.load_from_csv
  281. or
  282. Translation.load_from_csv(locale, file_location, file_charset) # e. g. 'en-us' or 'de-de' and /path/to/translation_list.csv
  283. e. g.
  284. Translation.load_from_csv('he-il', '/Users/me/Downloads/Hebrew_translation_list-1.csv', 'Windows-1255')
  285. Get source file at https://i18n.zammad.com/api/v1/translations_empty_translation_list
  286. =end
  287. def self.load_from_csv(locale_name, location, charset = 'UTF8')
  288. locale = Locale.find_by(locale: locale_name)
  289. if !locale
  290. raise "No such locale: #{locale_name}"
  291. end
  292. if !::File.exist?(location)
  293. raise "No such file: #{location}"
  294. end
  295. content = ::File.open(location, "r:#{charset}").read
  296. params = {
  297. col_sep: ',',
  298. }
  299. rows = ::CSV.parse(content, params)
  300. rows.shift # remove header
  301. translation_raw = []
  302. rows.each do |row|
  303. raise "Can't import translation, source is missing" if row[0].blank?
  304. if row[1].blank?
  305. warn "Skipped #{row[0]}, because translation is blank"
  306. next
  307. end
  308. raise "Can't import translation, format is missing" if row[2].blank?
  309. raise "Can't import translation, format is invalid (#{row[2]})" if !row[2].match?(%r{^(time|string)$})
  310. item = {
  311. 'locale' => locale.locale,
  312. 'source' => row[0],
  313. 'target' => row[1],
  314. 'target_initial' => '',
  315. 'format' => row[2],
  316. }
  317. translation_raw.push item
  318. end
  319. to_database(locale.name, translation_raw)
  320. true
  321. end
  322. def self.remote_translation_need_update?(raw, translations)
  323. translations.each do |row|
  324. next if row[1] != raw['locale']
  325. next if row[2] != raw['source']
  326. next if row[3] != raw['format']
  327. return false if row[4] == raw['target'] # no update if target is still the same
  328. return false if row[4] != row[5] # no update if translation has already changed
  329. return [true, Translation.find(row[0])]
  330. end
  331. [true, nil]
  332. end
  333. private_class_method def self.to_database(locale, data)
  334. translations = Translation.where(locale: locale).pluck(:id, :locale, :source, :format, :target, :target_initial).to_a
  335. ActiveRecord::Base.transaction do
  336. translations_to_import = []
  337. data.each do |translation_raw|
  338. result = Translation.remote_translation_need_update?(translation_raw, translations)
  339. next if result == false
  340. next if result.class != Array
  341. if result[1]
  342. result[1].update!(translation_raw.symbolize_keys!)
  343. result[1].save
  344. else
  345. translation_raw['updated_by_id'] = UserInfo.current_user_id || 1
  346. translation_raw['created_by_id'] = UserInfo.current_user_id || 1
  347. translations_to_import.push Translation.new(translation_raw.symbolize_keys!)
  348. end
  349. end
  350. if translations_to_import.present?
  351. Translation.import translations_to_import
  352. end
  353. end
  354. end
  355. private_class_method def self.locals_to_sync(dedicated_locale = nil)
  356. locales_list = []
  357. if dedicated_locale
  358. locales_list = [dedicated_locale]
  359. else
  360. locales = Locale.to_sync
  361. locales.each do |locale|
  362. locales_list.push locale.locale
  363. end
  364. end
  365. locales_list
  366. end
  367. private
  368. def set_initial
  369. return true if target_initial.present?
  370. return true if target_initial == ''
  371. self.target_initial = target
  372. true
  373. end
  374. def cache_clear
  375. Cache.delete("TranslationMapOnlyContent::#{locale.downcase}")
  376. true
  377. end
  378. def self.cache_set(locale, data)
  379. Cache.write("TranslationMapOnlyContent::#{locale.downcase}", data)
  380. end
  381. private_class_method :cache_set
  382. def self.cache_get(locale)
  383. Cache.read("TranslationMapOnlyContent::#{locale.downcase}")
  384. end
  385. private_class_method :cache_get
  386. end