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.empty?
  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 || translation.target_initial.empty?
  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. private_class_method def self.to_database(locale, data)
  261. translations = Translation.where(locale: locale).all
  262. ActiveRecord::Base.transaction do
  263. data.each do |translation_raw|
  264. # handle case insensitive sql
  265. translation = nil
  266. translations.each do |item|
  267. next if item.format != translation_raw['format']
  268. next if item.source != translation_raw['source']
  269. translation = item
  270. break
  271. end
  272. if translation
  273. # verify if update is needed
  274. update_needed = false
  275. translation_raw.each do |key, _value|
  276. # if translation target has changes
  277. next unless translation_raw[key] != translation.target
  278. # do not update translations which are already changed by user
  279. if translation.target == translation.target_initial
  280. update_needed = true
  281. break
  282. end
  283. end
  284. if update_needed
  285. translation.update!(translation_raw.symbolize_keys!)
  286. translation.save
  287. end
  288. else
  289. if !UserInfo.current_user_id
  290. translation_raw['updated_by_id'] = 1
  291. translation_raw['created_by_id'] = 1
  292. end
  293. Translation.create(translation_raw.symbolize_keys!)
  294. end
  295. end
  296. end
  297. end
  298. private_class_method def self.locals_to_sync(dedicated_locale = nil)
  299. locales_list = []
  300. if !dedicated_locale
  301. locales = Locale.to_sync
  302. locales.each do |locale|
  303. locales_list.push locale.locale
  304. end
  305. else
  306. locales_list = [dedicated_locale]
  307. end
  308. locales_list
  309. end
  310. private
  311. def set_initial
  312. return true if target_initial
  313. return true if target_initial == ''
  314. self.target_initial = target
  315. true
  316. end
  317. def cache_clear
  318. Cache.delete('TranslationMapOnlyContent::' + locale.downcase)
  319. true
  320. end
  321. def self.cache_set(locale, data)
  322. Cache.write('TranslationMapOnlyContent::' + locale.downcase, data)
  323. end
  324. private_class_method :cache_set
  325. def self.cache_get(locale)
  326. Cache.get('TranslationMapOnlyContent::' + locale.downcase)
  327. end
  328. private_class_method :cache_get
  329. end