txbr 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+
4
+ module Txbr
5
+ class BrazeApi
6
+ include RequestMethods
7
+
8
+ attr_reader :api_key, :api_url
9
+
10
+ def initialize(api_key, api_url)
11
+ @api_key = api_key
12
+ @api_url = api_url
13
+ end
14
+
15
+ def each_email_template
16
+ raise NotImplementedError, 'Braze does not support this operation yet'
17
+ end
18
+
19
+ def get_email_template_details(email_template_id:)
20
+ raise NotImplementedError, 'Braze does not support this operation yet'
21
+ end
22
+
23
+ private
24
+
25
+ def connection
26
+ @connection ||= begin
27
+ options = {
28
+ url: api_url,
29
+ params: { api_key: api_key },
30
+ headers: { Accept: 'application/json' }
31
+ }
32
+
33
+ Faraday.new(options) do |faraday|
34
+ faraday.request(:json)
35
+ faraday.response(:logger)
36
+ faraday.use(FaradayMiddleware::FollowRedirects)
37
+ faraday.adapter(:net_http)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,39 @@
1
+ require 'cgi'
2
+ require 'mechanize'
3
+
4
+ module Txbr
5
+ class BrazeSession
6
+ attr_reader :api_url, :email_address, :password
7
+
8
+ def initialize(api_url, email_address, password)
9
+ @api_url = api_url
10
+ @email_address = email_address
11
+ @password = password
12
+
13
+ reset!
14
+ end
15
+
16
+ def session_id
17
+ @session_id ||= begin
18
+ agent = Mechanize.new
19
+ url = Txbr::Utils.url_join(api_url, "auth?email=#{CGI.escape(email_address)}")
20
+
21
+ agent.get(url) do |page|
22
+ page.form_with(id: 'developer_signin').tap do |form|
23
+ form['developer[password]'] = password
24
+ form.submit
25
+ end
26
+ end
27
+
28
+ agent
29
+ .cookies
30
+ .find { |cookie| cookie.name == '_session_id' }
31
+ .value
32
+ end
33
+ end
34
+
35
+ def reset!
36
+ @session_id = nil
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,73 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+
4
+ module Txbr
5
+ class BrazeSessionApi
6
+ EMAIL_TEMPLATE_BATCH_SIZE = 35
7
+
8
+ include RequestMethods
9
+
10
+ attr_reader :session, :app_group_id
11
+
12
+ def initialize(session, app_group_id, connection: nil)
13
+ @session = session
14
+ @app_group_id = app_group_id
15
+ @connection = connection
16
+ end
17
+
18
+ def each_email_template(start: 0, &block)
19
+ return to_enum(__method__, start: start) unless block_given?
20
+
21
+ loop do
22
+ templates = get_json(
23
+ 'engagement/email_templates',
24
+ start: start,
25
+ limit: EMAIL_TEMPLATE_BATCH_SIZE
26
+ )
27
+
28
+ templates['results'].each(&block)
29
+ start += templates['results'].size
30
+ break if templates['results'].size < EMAIL_TEMPLATE_BATCH_SIZE
31
+ end
32
+ end
33
+
34
+ def get_email_template_details(email_template_id:)
35
+ get_json("engagement/email_templates/#{email_template_id}")
36
+ end
37
+
38
+ private
39
+
40
+ def act(*args)
41
+ retried = false
42
+
43
+ begin
44
+ super(*args, { cookie: "_session_id=#{session.session_id}" })
45
+ rescue BrazeUnauthorizedError => e
46
+ raise e if retried
47
+ reset!
48
+ retried = true
49
+ retry
50
+ end
51
+ end
52
+
53
+ def reset!
54
+ session.reset!
55
+ end
56
+
57
+ def connection
58
+ @connection ||= begin
59
+ options = {
60
+ url: session.api_url,
61
+ params: { app_group_id: app_group_id }
62
+ }
63
+
64
+ Faraday.new(options) do |faraday|
65
+ faraday.request(:json)
66
+ faraday.response(:logger)
67
+ faraday.use(FaradayMiddleware::FollowRedirects)
68
+ faraday.adapter(:net_http)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,14 @@
1
+ module Txbr
2
+ module Commands
3
+ def self.upload_all
4
+ Txbr::Config.projects.each do |project|
5
+ begin
6
+ Txbr::Uploader.new(project).upload_all
7
+ rescue => e
8
+ puts "An error occurred: #{e.message}"
9
+ puts e.backtrace
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,57 @@
1
+ require 'yaml'
2
+
3
+ module Txbr
4
+ class Config
5
+ class << self
6
+ def projects
7
+ @projects ||= raw_config[:projects].map do |conf|
8
+ Project.new(conf)
9
+ end
10
+ end
11
+
12
+ def transifex_api_username
13
+ raw_config[:transifex_api_username]
14
+ end
15
+
16
+ def transifex_api_password
17
+ raw_config[:transifex_api_password]
18
+ end
19
+
20
+ private
21
+
22
+ def raw_config
23
+ @raw_config ||= begin
24
+ scheme_end_idx = ENV['TXBR_CONFIG'].index('://')
25
+ scheme = ENV['TXBR_CONFIG'][0...scheme_end_idx]
26
+ payload = ENV['TXBR_CONFIG'][(scheme_end_idx + 3)..-1]
27
+ send(:"load_#{scheme}", payload)
28
+ end
29
+ end
30
+
31
+ def load_file(payload)
32
+ deep_symbolize_keys(YAML.load_file(payload))
33
+ end
34
+
35
+ def load_raw(payload)
36
+ deep_symbolize_keys(YAML.load(payload))
37
+ end
38
+
39
+ def deep_symbolize_keys(obj)
40
+ case obj
41
+ when Hash
42
+ obj.each_with_object({}) do |(k, v), ret|
43
+ ret[k.to_sym] = deep_symbolize_keys(v)
44
+ end
45
+
46
+ when Array
47
+ obj.map do |elem|
48
+ deep_symbolize_keys(elem)
49
+ end
50
+
51
+ else
52
+ obj
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,70 @@
1
+ require 'liquid'
2
+
3
+ module Txbr
4
+ class EmailTemplate
5
+ attr_reader :project, :email_template_id
6
+
7
+ def initialize(project, email_template_id)
8
+ @project = project
9
+ @email_template_id = email_template_id
10
+ end
11
+
12
+ def each_resource
13
+ return to_enum(__method__) unless block_given?
14
+
15
+ strings_map.each_pair do |project_slug, resource_map|
16
+ resource_map.each_pair do |resource_slug, strings_manifest|
17
+ phrases = strings_manifest.each_string
18
+ .reject { |_, value| value.nil? }
19
+ .map do |path, value|
20
+ { 'key' => path.join('.'), 'string' => value }
21
+ end
22
+
23
+ next if phrases.empty?
24
+
25
+ resource = Txgh::TxResource.new(
26
+ project_slug,
27
+ resource_slug,
28
+ project.strings_format,
29
+ project.source_lang,
30
+ template_name,
31
+ {}, # lang_map (none)
32
+ nil # translation_file (none)
33
+ )
34
+
35
+ yield Txgh::ResourceContents.from_phrase_list(resource, phrases)
36
+ end
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def strings_map
43
+ @strings_map ||= %w(template subject preheader).each_with_object({}) do |name, ret|
44
+ component = EmailTemplateComponent.new(
45
+ Liquid::Template.parse(details[name])
46
+ )
47
+
48
+ next unless component.assignments['project_slug']
49
+ next unless component.assignments['resource_slug']
50
+ next unless component.translation_enabled?
51
+
52
+ (ret[component.project_slug] ||= {}).tap do |proj_map|
53
+ (proj_map[component.resource_slug] ||= StringsManifest.new).tap do |manifest|
54
+ manifest.merge!(component.strings)
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ def template_name
61
+ details['name']
62
+ end
63
+
64
+ def details
65
+ @details ||= project.braze_api.get_email_template_details(
66
+ email_template_id: email_template_id
67
+ )
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,94 @@
1
+ module Txbr
2
+ class EmailTemplateComponent
3
+ ASSIGNMENTS = %w(project_slug resource_slug translation_enabled)
4
+
5
+ attr_reader :liquid_template
6
+
7
+ def initialize(liquid_template)
8
+ @liquid_template = liquid_template
9
+ end
10
+
11
+ def project_slug
12
+ # blow up with KeyError if not found
13
+ assignments.fetch('project_slug')
14
+ end
15
+
16
+ def resource_slug
17
+ # blow up with KeyError if not found
18
+ assignments.fetch('resource_slug')
19
+ end
20
+
21
+ def translation_enabled?
22
+ # translation is disabled by default
23
+ assignments.fetch('translation_enabled', true)
24
+ end
25
+
26
+ def assignments
27
+ @assignments ||= {}.tap do |assgn|
28
+ liquid_template.root.nodelist.each do |node|
29
+ case node
30
+ when Liquid::Assign
31
+ to = node.instance_variable_get(:@to)
32
+
33
+ if ASSIGNMENTS.include?(to)
34
+ from = node.instance_variable_get(:@from).name
35
+ assgn[to] = from
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def strings
43
+ @strings ||= StringsManifest.new.tap do |manifest|
44
+ extract_strings_from(liquid_template.root, manifest)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def extract_strings_from(root, manifest)
51
+ return unless root.nodelist
52
+
53
+ root.nodelist.each do |node|
54
+ case node
55
+ # We only care about Liquid variables, which are written
56
+ # like {{prefix.foo.bar}}. We identify the prefix (i.e.
57
+ # the first lookup, or path segment) to verify it's
58
+ # associated with a connected_content call. Then we add
59
+ # the prefix and the rest of the lookups to the strings
60
+ # manifest along with the value. The prefix is used to
61
+ # divide the strings into individual Transifex resources
62
+ # while the rest of the lookups form the string's key.
63
+ when Liquid::Variable
64
+ prefix = node.name.name
65
+ path = node.name.lookups
66
+
67
+ # the English translation (or whatever language your
68
+ # source strings are written in) is provided using
69
+ # Liquid's built-in "default" filter
70
+ next unless connected_content_prefixes.include?(prefix)
71
+ default = node.filters.find { |f| f.first == 'default' }
72
+
73
+ manifest.add(path, default&.last&.first)
74
+ end
75
+
76
+ if node.respond_to?(:nodelist)
77
+ extract_strings_from(node, manifest)
78
+ end
79
+ end
80
+ end
81
+
82
+ def connected_content_prefixes
83
+ @connected_content_prefixes ||= connected_content_tags.map(&:prefix)
84
+ end
85
+
86
+ def connected_content_tags
87
+ # this assumes these are basically at the top of the template and not
88
+ # nested inside other liquid tags
89
+ @connected_content_tags ||= liquid_template.root.nodelist.select do |node|
90
+ node.is_a?(Txbr::ConnectedContentTag)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,24 @@
1
+ module Txbr
2
+ class EmailTemplateHandler
3
+ attr_reader :project
4
+
5
+ def initialize(project)
6
+ @project = project
7
+ end
8
+
9
+ def each_resource(&block)
10
+ return to_enum(__method__) unless block_given?
11
+ each_template { |tmpl| tmpl.each_resource(&block) }
12
+ end
13
+
14
+ private
15
+
16
+ def each_template
17
+ return to_enum(__method__) unless block_given?
18
+
19
+ project.braze_api.each_email_template do |tmpl|
20
+ yield EmailTemplate.new(project, tmpl['id'])
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ require 'txgh'
2
+
3
+ module Txbr
4
+ class Project
5
+ attr_reader :braze_api_url, :braze_api_key, :handler_id
6
+ attr_reader :strings_format, :source_lang
7
+
8
+ # @TODO: remove these when Braze gives us the endpoints we asked for
9
+ attr_reader :braze_app_group_id, :braze_email_address, :braze_password
10
+
11
+ def initialize(options = {})
12
+ @braze_api_url = options.fetch(:braze_api_url)
13
+ @braze_api_key = options.fetch(:braze_api_key)
14
+ @handler_id = options.fetch(:handler_id)
15
+ @strings_format = options.fetch(:strings_format)
16
+ @source_lang = options.fetch(:source_lang)
17
+
18
+ # @TODO: remove these when Braze gives us the endpoints we asked for
19
+ @braze_app_group_id = options.fetch(:braze_app_group_id)
20
+ @braze_email_address = options.fetch(:braze_email_address)
21
+ @braze_password = options.fetch(:braze_password)
22
+
23
+ @braze_api = options[:braze_api]
24
+ @transifex_api = options[:transifex_api]
25
+ end
26
+
27
+ def braze_session
28
+ @@braze_session ||= Txbr::BrazeSession.new(
29
+ braze_api_url, braze_email_address, braze_password
30
+ )
31
+ end
32
+
33
+ def braze_api
34
+ # @TODO: use BrazeApi when Braze gives us the endpoints we asked for
35
+ # @braze_api ||= Txbr::BrazeApi.new(braze_api_key, braze_api_url)
36
+ @braze_api ||= Txbr::BrazeSessionApi.new(braze_session, braze_app_group_id)
37
+ end
38
+
39
+ def transifex_api
40
+ @transifex_api ||= Txgh::TransifexApi.create_from_credentials(
41
+ Txbr::Config.transifex_api_username,
42
+ Txbr::Config.transifex_api_password
43
+ )
44
+ end
45
+
46
+ def handler
47
+ @handler ||= Txbr.handler_for(self)
48
+ end
49
+ end
50
+ end