translation.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  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. 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 = if admin
  108. [
  109. item.id,
  110. item.source,
  111. item.target,
  112. item.target_initial,
  113. item.format,
  114. ]
  115. else
  116. [
  117. item.id,
  118. item.source,
  119. item.target,
  120. item.format,
  121. ]
  122. end
  123. list.push translation_item
  124. end
  125. # add presorted on top
  126. presorted_list = []
  127. %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|
  128. list.each do |item|
  129. next if item[1] != presort
  130. presorted_list.push item
  131. list.delete item
  132. #list.unshift presort
  133. end
  134. end
  135. data['list'] = presorted_list.concat list
  136. # set cache
  137. if !admin
  138. cache_set(locale, data)
  139. end
  140. data
  141. end
  142. =begin
  143. translate strings in ruby context, e. g. for notifications
  144. translated = Translation.translate('de-de', 'New')
  145. =end
  146. def self.translate(locale, string)
  147. # translate string
  148. records = Translation.where(locale: locale, source: string)
  149. records.each do |record|
  150. return record.target if record.source == string
  151. end
  152. # fallback lookup in en
  153. records = Translation.where(locale: 'en', source: string)
  154. records.each do |record|
  155. return record.target if record.source == string
  156. end
  157. string
  158. end
  159. =begin
  160. translate timestampes in ruby context, e. g. for notifications
  161. translated = Translation.timestamp('de-de', 'Europe/Berlin', '2018-10-10T10:00:00Z0')
  162. or
  163. translated = Translation.timestamp('de-de', 'Europe/Berlin', Time.zone.parse('2018-10-10T10:00:00Z0'))
  164. =end
  165. def self.timestamp(locale, timezone, timestamp)
  166. if timestamp.class == String
  167. begin
  168. timestamp_parsed = Time.zone.parse(timestamp)
  169. return timestamp.to_s if !timestamp_parsed
  170. timestamp = timestamp_parsed
  171. rescue
  172. return timestamp.to_s
  173. end
  174. end
  175. begin
  176. timestamp = timestamp.in_time_zone(timezone)
  177. rescue
  178. return timestamp.to_s
  179. end
  180. record = Translation.where(locale: locale, source: 'timestamp', format: 'time').pluck(:target).first
  181. return timestamp.to_s if !record
  182. record.sub!('dd', format('%<day>02d', day: timestamp.day))
  183. record.sub!('d', timestamp.day.to_s)
  184. record.sub!('mm', format('%<month>02d', month: timestamp.month))
  185. record.sub!('m', timestamp.month.to_s)
  186. record.sub!('yyyy', timestamp.year.to_s)
  187. record.sub!('yy', timestamp.year.to_s.last(2))
  188. record.sub!('SS', format('%<second>02d', second: timestamp.sec.to_s))
  189. record.sub!('MM', format('%<minute>02d', minute: timestamp.min.to_s))
  190. record.sub!('HH', format('%<hour>02d', hour: timestamp.hour.to_s))
  191. "#{record} (#{timezone})"
  192. end
  193. =begin
  194. translate date in ruby context, e. g. for notifications
  195. translated = Translation.date('de-de', '2018-10-10')
  196. or
  197. translated = Translation.date('de-de', Date.parse('2018-10-10'))
  198. =end
  199. def self.date(locale, date)
  200. if date.class == String
  201. begin
  202. date_parsed = Date.parse(date)
  203. return date.to_s if !date_parsed
  204. date = date_parsed
  205. rescue
  206. return date.to_s
  207. end
  208. end
  209. return date.to_s if date.class != Date
  210. record = Translation.where(locale: locale, source: 'date', format: 'time').pluck(:target).first
  211. return date.to_s if !record
  212. record.sub!('dd', format('%<day>02d', day: date.day))
  213. record.sub!('d', date.day.to_s)
  214. record.sub!('mm', format('%<month>02d', month: date.month))
  215. record.sub!('m', date.month.to_s)
  216. record.sub!('yyyy', date.year.to_s)
  217. record.sub!('yy', date.year.to_s.last(2))
  218. record
  219. end
  220. =begin
  221. load translations from local
  222. all:
  223. Translation.load_from_file
  224. or
  225. Translation.load_from_file(locale) # e. g. 'en-us' or 'de-de'
  226. =end
  227. def self.load_from_file(dedicated_locale = nil)
  228. version = Version.get
  229. directory = Rails.root.join('config/translations')
  230. locals_to_sync(dedicated_locale).each do |locale|
  231. file = Rails.root.join(directory, "#{locale}-#{version}.yml")
  232. return false if !File.exist?(file)
  233. data = YAML.load_file(file)
  234. to_database(locale, data)
  235. end
  236. true
  237. end
  238. =begin
  239. fetch translation from remote and store them in local file system
  240. all:
  241. Translation.fetch
  242. or
  243. Translation.fetch(locale) # e. g. 'en-us' or 'de-de'
  244. =end
  245. def self.fetch(dedicated_locale = nil)
  246. version = Version.get
  247. locals_to_sync(dedicated_locale).each do |locale|
  248. url = "https://i18n.zammad.com/api/v1/translations/#{locale}"
  249. if !UserInfo.current_user_id
  250. UserInfo.current_user_id = 1
  251. end
  252. result = UserAgent.get(
  253. url,
  254. {
  255. version: version,
  256. },
  257. {
  258. json: true,
  259. open_timeout: 8,
  260. read_timeout: 24,
  261. }
  262. )
  263. raise "Can't load translations from #{url}: #{result.error}" if !result.success?
  264. directory = Rails.root.join('config/translations')
  265. if !File.directory?(directory)
  266. Dir.mkdir(directory, 0o755)
  267. end
  268. file = Rails.root.join(directory, "#{locale}-#{version}.yml")
  269. File.open(file, 'w') do |out|
  270. YAML.dump(result.data, out)
  271. end
  272. end
  273. true
  274. end
  275. =begin
  276. load translations from csv file
  277. all:
  278. Translation.load_from_csv
  279. or
  280. Translation.load_from_csv(locale, file_location, file_charset) # e. g. 'en-us' or 'de-de' and /path/to/translation_list.csv
  281. e. g.
  282. Translation.load_from_csv('he-il', '/Users/me/Downloads/Hebrew_translation_list-1.csv', 'Windows-1255')
  283. Get source file at https://i18n.zammad.com/api/v1/translations_empty_translation_list
  284. =end
  285. def self.load_from_csv(locale_name, location, charset = 'UTF8')
  286. locale = Locale.find_by(locale: locale_name)
  287. if !locale
  288. raise "No such locale: #{locale_name}"
  289. end
  290. if !::File.exist?(location)
  291. raise "No such file: #{location}"
  292. end
  293. content = ::File.open(location, "r:#{charset}").read
  294. params = {
  295. col_sep: ',',
  296. }
  297. rows = ::CSV.parse(content, params)
  298. rows.shift # remove header
  299. translation_raw = []
  300. rows.each do |row|
  301. raise "Can't import translation, source is missing" if row[0].blank?
  302. if row[1].blank?
  303. warn "Skipped #{row[0]}, because translation is blank"
  304. next
  305. end
  306. raise "Can't import translation, format is missing" if row[2].blank?
  307. raise "Can't import translation, format is invalid (#{row[2]})" if !row[2].match?(/^(time|string)$/)
  308. item = {
  309. 'locale' => locale.locale,
  310. 'source' => row[0],
  311. 'target' => row[1],
  312. 'target_initial' => '',
  313. 'format' => row[2],
  314. }
  315. translation_raw.push item
  316. end
  317. to_database(locale.name, translation_raw)
  318. true
  319. end
  320. def self.remote_translation_need_update?(raw, translations)
  321. translations.each do |row|
  322. next if row[1] != raw['locale']
  323. next if row[2] != raw['source']
  324. next if row[3] != raw['format']
  325. return false if row[4] == raw['target'] # no update if target is still the same
  326. return false if row[4] != row[5] # no update if translation has already changed
  327. return [true, Translation.find(row[0])]
  328. end
  329. [true, nil]
  330. end
  331. private_class_method def self.to_database(locale, data)
  332. translations = Translation.where(locale: locale).pluck(:id, :locale, :source, :format, :target, :target_initial).to_a
  333. ActiveRecord::Base.transaction do
  334. translations_to_import = []
  335. data.each do |translation_raw|
  336. result = Translation.remote_translation_need_update?(translation_raw, translations)
  337. next if result == false
  338. next if result.class != Array
  339. if result[1]
  340. result[1].update!(translation_raw.symbolize_keys!)
  341. result[1].save
  342. else
  343. translation_raw['updated_by_id'] = UserInfo.current_user_id || 1
  344. translation_raw['created_by_id'] = UserInfo.current_user_id || 1
  345. translations_to_import.push Translation.new(translation_raw.symbolize_keys!)
  346. end
  347. end
  348. if translations_to_import.present?
  349. Translation.import translations_to_import
  350. end
  351. end
  352. end
  353. private_class_method def self.locals_to_sync(dedicated_locale = nil)
  354. locales_list = []
  355. if !dedicated_locale
  356. locales = Locale.to_sync
  357. locales.each do |locale|
  358. locales_list.push locale.locale
  359. end
  360. else
  361. locales_list = [dedicated_locale]
  362. end
  363. locales_list
  364. end
  365. private
  366. def set_initial
  367. return true if target_initial.present?
  368. return true if target_initial == ''
  369. self.target_initial = target
  370. true
  371. end
  372. def cache_clear
  373. Cache.delete('TranslationMapOnlyContent::' + locale.downcase)
  374. true
  375. end
  376. def self.cache_set(locale, data)
  377. Cache.write('TranslationMapOnlyContent::' + locale.downcase, data)
  378. end
  379. private_class_method :cache_set
  380. def self.cache_get(locale)
  381. Cache.get('TranslationMapOnlyContent::' + locale.downcase)
  382. end
  383. private_class_method :cache_get
  384. end