synchronizes_from_po.rb 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. # Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
  2. module Translation::SynchronizesFromPo
  3. extend ActiveSupport::Concern
  4. TRANSLATION_FILE_STRUCT = Struct.new(:translation, :translation_file, keyword_init: true).freeze
  5. class_methods do # rubocop:disable Metrics/BlockLength
  6. def sync
  7. Locale.to_sync.each do |locale|
  8. ActiveRecord::Base.transaction do
  9. sync_locale_from_po locale.locale
  10. end
  11. end
  12. end
  13. def sync_locale_from_po(locale) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  14. previous_unmodified_translations = Translation.where(locale: locale, is_synchronized_from_codebase: true).select { |t| t.target.eql?(t.target_initial) }
  15. updated_translation_ids = Set[]
  16. importable_translations = []
  17. strings_for_locale(locale).each_pair do |source, entry| # rubocop:disable Metrics/BlockLength
  18. if source.length > 3000 || entry.translation.length > 3000
  19. Rails.logger.error "Cannot import translation for locale #{locale} because it exceeds maximum string length of 3000: source: '#{source}', translation: '#{entry.translation}'"
  20. next
  21. end
  22. t = Translation.find_source(locale, source)
  23. # New string
  24. if !t
  25. importable_translations << Translation.new(
  26. locale: locale,
  27. source: source,
  28. target: entry.translation,
  29. target_initial: entry.translation,
  30. is_synchronized_from_codebase: true,
  31. synchronized_from_translation_file: entry.translation_file,
  32. created_by_id: 1,
  33. updated_by_id: 1
  34. )
  35. next
  36. end
  37. # Existing string
  38. # Only change the target if it was not modified by the user
  39. t.target = entry.translation if t.target.eql? t.target_initial
  40. t.is_synchronized_from_codebase = true
  41. t.synchronized_from_translation_file = entry.translation_file
  42. t.target_initial = entry.translation
  43. if t.changed.present?
  44. t.updated_by_id = 1
  45. t.save!
  46. end
  47. updated_translation_ids.add t.id
  48. end
  49. Translation.import importable_translations
  50. # Remove any unmodified & synchronized strings that are not present in the data any more.
  51. previous_unmodified_translations.reject { |t| updated_translation_ids.member? t.id }.each(&:destroy!)
  52. true
  53. end
  54. def strings_for_locale(locale) # rubocop:disable Metrics/AbcSize
  55. result = {}
  56. po_files_for_locale(locale).each do |file|
  57. require 'poparser' # Only load when it is actually used
  58. PoParser.parse_file(Rails.root.join(file)).entries.each do |entry|
  59. source = unescape_po(entry.msgid.to_s)
  60. # Make sure to ignore fuzzy entries.
  61. translation = entry.translated? ? unescape_po(entry.msgstr.to_s) : ''
  62. # For 'en-*' locales, treat source as translation as well, to indicate that nothing is missing.
  63. translation = source if translation.empty? && locale.start_with?('en')
  64. result[source] = TRANSLATION_FILE_STRUCT.new(translation: translation, translation_file: file)
  65. end
  66. end
  67. result
  68. end
  69. def unescape_po(string)
  70. string.gsub(%r{\\n}, "\n").gsub(%r{\\"}, '"').gsub(%r{\\\\}, '\\')
  71. end
  72. # Returns all po files for a locale with zammad.*.po as first entry,
  73. # followed by all other files in alphabetical order
  74. # For en-us, i18n/zammad.pot will be returned instead.
  75. def po_files_for_locale(locale)
  76. return ['i18n/zammad.pot'] if locale.eql? 'en-us'
  77. files = Dir.glob "i18n/*.#{locale}.po", base: Rails.root
  78. if files.exclude?("i18n/zammad.#{locale}.po")
  79. Rails.logger.error "No translation found for locale '#{locale}'."
  80. return []
  81. end
  82. [
  83. files.delete("i18n/zammad.#{locale}.po"),
  84. *files.sort
  85. ]
  86. end
  87. end
  88. end