google_translation_agent.rb 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. module Agents
  2. class GoogleTranslationAgent < Agent
  3. cannot_be_scheduled!
  4. can_dry_run!
  5. gem_dependency_check do
  6. require 'google/cloud/translate/v2'
  7. rescue LoadError
  8. false
  9. else
  10. true
  11. end
  12. description <<~MD
  13. The Translation Agent will attempt to translate text between natural languages.
  14. #{'## Include `google-api-client` in your Gemfile to use this Agent!' if dependencies_missing?}
  15. Services are provided using Google Translate. You can [sign up](https://cloud.google.com/translate/) to get `google_api_key` which is required to use this agent.
  16. The service is **not free**.
  17. To use credentials for the `google_api_key` use the liquid `credential` tag like so `{% credential google-api-key %}`
  18. `to` must be filled with a [translator language code](https://cloud.google.com/translate/docs/languages).
  19. `from` is the language translated from. If it's not specified, the API will attempt to detect the source language automatically and return it within the response.
  20. Specify an object in `content` field using [Liquid](https://github.com/huginn/huginn/wiki/Formatting-Events-using-Liquid) expressions, which will be evaluated for each incoming event, and then translated to become the payload of the new event.
  21. You can specify a nested object of any levels containing arrays and objects, and all string values except for object keys will be recursively translated.
  22. Set `mode` to `merge` if you want to merge each translated content with the original event payload. The default behavior (`clean`) is to emit events with only translated contents.
  23. `expected_receive_period_in_days` is the maximum number of days you would allow to pass between events.
  24. MD
  25. event_description "User defined"
  26. def default_options
  27. {
  28. 'mode' => 'clean',
  29. 'to' => 'sv',
  30. 'from' => 'en',
  31. 'google_api_key' => '',
  32. 'expected_receive_period_in_days' => 1,
  33. 'content' => {
  34. 'text' => "{{message}}",
  35. 'moretext' => "{{another_message}}"
  36. }
  37. }
  38. end
  39. def working?
  40. last_receive_at && last_receive_at > interpolated['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
  41. end
  42. def validate_options
  43. unless options['google_api_key'].present? && options['to'].present? && options['content'].present? && options['expected_receive_period_in_days'].present?
  44. errors.add :base, "google_api_key, to, content and expected_receive_period_in_days are all required"
  45. end
  46. case options['mode'].presence
  47. when nil, /\A(?:clean|merge)\z|\{/
  48. # ok
  49. else
  50. errors.add(:base, "mode must be 'clean' or 'merge'")
  51. end
  52. end
  53. def receive(incoming_events)
  54. incoming_events.each do |event|
  55. interpolate_with(event) do
  56. translated_content = translate(interpolated['content'])
  57. case interpolated['mode']
  58. when 'merge'
  59. create_event payload: event.payload.merge(translated_content)
  60. else
  61. create_event payload: translated_content
  62. end
  63. end
  64. end
  65. end
  66. def translate(content)
  67. if !content.is_a?(Hash)
  68. error("content must be an object, but it is #{content.class}.")
  69. return
  70. end
  71. api = Google::Cloud::Translate::V2.new(
  72. key: interpolated['google_api_key']
  73. )
  74. texts = []
  75. walker = ->(value) {
  76. case value
  77. in nil | Numeric | true | false
  78. in _ if _.blank?
  79. in String
  80. texts << value
  81. in Array
  82. value.each(&walker)
  83. in Hash
  84. value.each_value(&walker)
  85. end
  86. }
  87. walker.call(content)
  88. translations =
  89. if texts.empty?
  90. []
  91. else
  92. api.translate(
  93. *texts,
  94. from: interpolated['from'].presence,
  95. to: interpolated['to'],
  96. format: 'text',
  97. )
  98. end
  99. # Hash key order should be constant in Ruby
  100. mapper = ->(value) {
  101. case value
  102. in nil | Numeric | true | false
  103. value
  104. in _ if _.blank?
  105. value
  106. in String
  107. translations&.shift&.text
  108. in Array
  109. value.map(&mapper)
  110. in Hash
  111. value.transform_values(&mapper)
  112. end
  113. }
  114. mapper.call(content)
  115. end
  116. end
  117. end