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. 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. 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.class == 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. record = Translation.where(locale: locale, source: 'timestamp', format: 'time').pluck(:target).first
  177. return timestamp.to_s if !record
  178. begin
  179. timestamp = timestamp.in_time_zone(timezone)
  180. rescue
  181. return timestamp.to_s
  182. end
  183. record.sub!('dd', format('%02d', timestamp.day))
  184. record.sub!('d', timestamp.day.to_s)
  185. record.sub!('mm', format('%02d', 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('%02d', timestamp.sec.to_s))
  190. record.sub!('MM', format('%02d', timestamp.min.to_s))
  191. record.sub!('HH', format('%02d', 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.class == 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').pluck(:target).first
  212. return date.to_s if !record
  213. record.sub!('dd', format('%02d', date.day))
  214. record.sub!('d', date.day.to_s)
  215. record.sub!('mm', format('%02d', 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. }
  263. )
  264. raise "Can't load translations from #{url}: #{result.error}" if !result.success?
  265. directory = Rails.root.join('config', 'translations')
  266. if !File.directory?(directory)
  267. Dir.mkdir(directory, 0o755)
  268. end
  269. file = Rails.root.join(directory, "#{locale}-#{version}.yml")
  270. File.open(file, 'w') do |out|
  271. YAML.dump(result.data, out)
  272. end
  273. end
  274. true
  275. end
  276. =begin
  277. load translations from csv file
  278. all:
  279. Translation.load_from_csv
  280. or
  281. Translation.load_from_csv(locale, file_location, file_charset) # e. g. 'en-us' or 'de-de' and /path/to/translation_list.csv
  282. e. g.
  283. Translation.load_from_csv('he-il', '/Users/me/Downloads/Hebrew_translation_list-1.csv', 'Windows-1255')
  284. Get source file at https://i18n.zammad.com/api/v1/translations_empty_translation_list
  285. =end
  286. def self.load_from_csv(locale_name, location, charset = 'UTF8')
  287. locale = Locale.find_by(locale: locale_name)
  288. if !locale
  289. raise "No such locale: #{locale_name}"
  290. end
  291. if !::File.exist?(location)
  292. raise "No such file: #{location}"
  293. end
  294. content = ::File.open(location, "r:#{charset}").read
  295. params = {
  296. col_sep: ',',
  297. }
  298. rows = ::CSV.parse(content, params)
  299. header = rows.shift
  300. translation_raw = []
  301. rows.each do |row|
  302. raise "Can't import translation, source is missing" if row[0].blank?
  303. if row[1].blank?
  304. warn "Skipped #{row[0]}, because translation is blank"
  305. next
  306. end
  307. raise "Can't import translation, format is missing" if row[2].blank?
  308. raise "Can't import translation, format is invalid (#{row[2]})" if row[2] !~ /^(time|string)$/
  309. item = {
  310. 'locale' => locale.locale,
  311. 'source' => row[0],
  312. 'target' => row[1],
  313. 'target_initial' => '',
  314. 'format' => row[2],
  315. }
  316. translation_raw.push item
  317. end
  318. to_database(locale.name, translation_raw)
  319. true
  320. end
  321. def self.remote_translation_need_update?(raw, translations)
  322. translations.each do |row|
  323. next if row[1] != raw['locale']
  324. next if row[2] != raw['source']
  325. next if row[3] != raw['format']
  326. return false if row[4] == raw['target'] # no update if target is still the same
  327. return false if row[4] != row[5] # no update if translation has already changed
  328. return [true, Translation.find(row[0])]
  329. end
  330. [true, nil]
  331. end
  332. private_class_method def self.to_database(locale, data)
  333. translations = Translation.where(locale: locale).pluck(:id, :locale, :source, :format, :target, :target_initial).to_a
  334. ActiveRecord::Base.transaction do
  335. translations_to_import = []
  336. data.each do |translation_raw|
  337. result = Translation.remote_translation_need_update?(translation_raw, translations)
  338. next if result == false
  339. next if result.class != Array
  340. if result[1]
  341. result[1].update!(translation_raw.symbolize_keys!)
  342. result[1].save
  343. else
  344. translation_raw['updated_by_id'] = UserInfo.current_user_id || 1
  345. translation_raw['created_by_id'] = UserInfo.current_user_id || 1
  346. translations_to_import.push Translation.new(translation_raw.symbolize_keys!)
  347. end
  348. end
  349. if translations_to_import.present?
  350. Translation.import translations_to_import
  351. end
  352. end
  353. end
  354. private_class_method def self.locals_to_sync(dedicated_locale = nil)
  355. locales_list = []
  356. if !dedicated_locale
  357. locales = Locale.to_sync
  358. locales.each do |locale|
  359. locales_list.push locale.locale
  360. end
  361. else
  362. locales_list = [dedicated_locale]
  363. end
  364. locales_list
  365. end
  366. private
  367. def set_initial
  368. return true if target_initial.present?
  369. return true if target_initial == ''
  370. self.target_initial = target
  371. true
  372. end
  373. def cache_clear
  374. Cache.delete('TranslationMapOnlyContent::' + locale.downcase)
  375. true
  376. end
  377. def self.cache_set(locale, data)
  378. Cache.write('TranslationMapOnlyContent::' + locale.downcase, data)
  379. end
  380. private_class_method :cache_set
  381. def self.cache_get(locale)
  382. Cache.get('TranslationMapOnlyContent::' + locale.downcase)
  383. end
  384. private_class_method :cache_get
  385. end