txbr 1.1.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 62e08fd199a92f5d5f3023cd24e118384402b8124cfb70922e06ac727a8ed609
4
- data.tar.gz: fb507665ce3f234fcab1c1b8a34954b3458f23639f41dd5b4b53423c8d2725ff
3
+ metadata.gz: 9d1499776cb6db1539a2d2701bfddd4e182f9b3246b28d13e245e97426002902
4
+ data.tar.gz: 4b4ad8fb50db7bc76fa87e0435df0de19e0d6809dc215584eb0e8b403de97b5c
5
5
  SHA512:
6
- metadata.gz: 55a839b383684e07541c2af875074e69c7fe5eff1d0d90f1a684b1d4621a226d4550bf611e5186f7d0ed81775d9d89ee8f7e9305eeb5ee458fdbc198e257300d
7
- data.tar.gz: 401b4b866e1805a26a782aba41ceb62a200a3aab02591279ed149b92282af4c048be93160d666d8b052feff0bf0fb97fe68a30c9733462f3ba2d805b242353fb
6
+ metadata.gz: da9cf5bef80c81a529717678d384a5e3eae78d26b8e4cffd0b53ca78ac8cd4cb10690d5a0211b16082ca20f719a60345e63a62be3b9598f9e1290322135485a3
7
+ data.tar.gz: 6d026e747fda2c285f3a792b46babbe2dc85e71d7fb27168b097efaac820188fc2c7a75cb6e5a0b7d8a7162e9503ace863674cce4635f2e655053c2f36da990a
data/README.md CHANGED
@@ -17,7 +17,7 @@ Here's an example template for the impatient:
17
17
  <head>
18
18
  {% assign project_slug = "my_transifex_project" %}
19
19
  {% assign resource_slug = "my_transifex_resource" %}
20
- {% connected_content https://your_txbr_server.com/strings.json?project_slug={{project_slug}}&resource_slug={{resource_slug}}&locale={{${language} | default: 'en'}}&strings_format=YML :basic_auth txbr :save strings %}
20
+ {% connected_content https://your_txbr_server.com/strings.json?project_slug={{project_slug}}&resource_slug={{resource_slug}}&locale={{${language} | default: 'en'}}&strings_format=YML :basic_auth txbr :save strings :retry %}
21
21
  </head>
22
22
  <body>
23
23
  {{strings.header.title | default: "Buy my stuff!"}}
@@ -25,13 +25,17 @@ Here's an example template for the impatient:
25
25
  </html>
26
26
  ```
27
27
 
28
- There are several important bits in the example above:
28
+ ### The `connected_content` Tag
29
29
 
30
- 1. Every template you would like Txbr to manage must contain these three required Liquid tags: `assign project_slug`, `assign resource_slug`, and `connected_content`. The first two correspond to the Transifex project and resource in which you would like to store the template's strings, and the third is the mechanism by which translations are fetched and inserted into the template when it is previewed or delivered.
31
- 2. The project slug should correspond to a valid Transifex project. You'll need to create the project in Transifex, then copy the slug from the URL.
32
- 3. The resource slug should be unique within the Transifex project and must only contain uppercase and lowercase letters, numbers, underscores (i.e. "_"), and dashes (i.e. "-"). It's a safe bet to simply use the template's API identifier found at the bottom of the template's configuration page. Txbr itself places no restrictons on and performs no validation against this field, so it can contain any custom slug you want.
33
- 4. The `connected_content` tag fetches translated content from Transifex using the project and resource slugs you assigned earlier in the locale of the "current" user. When previewing your template, the locale can be set in the left-hand sidebar via a simulated current user.
34
- 5. Notice the `{{strings.header.title | default: "Buy my stuff!}}` tag. This defines and fetches a string at the key `header.title`, with a default English value of "Buy my stuff!" Txbr uses this key and default value to construct the translation file it will submit to Transifex. Providing a default value allows for easy template construction and previewing, since your template probably won't be translated immediately. Braze will fall back to this value if the translation doesn't yet exist. Liquid tags that do not specify a default value will not be included in the submission to Transifex. Finally, pay close attention to the `strings.` prefix. It corresponds to the `connected_content` tag's `:save strings` option. The `:save` option tells Braze to store the translated strings in a template variable called `strings`, which can then be used to grab individual strings. The value given to `:save` must be the same value used as the key prefix. For example, simply typing `{{header.title}}` won't work.
30
+ Every template you would like Txbr to manage should include at least one `connected_content` tag. Each tag must include a URL with `project_slug` and `resource_slug` as GET parameters. These correspond to the Transifex project and resource you would like to store the template's strings in.
31
+
32
+ The project slug should correspond to a valid Transifex project. You'll need to create the project in Transifex, then copy the slug from the URL.
33
+
34
+ The resource slug should be unique within the Transifex project and must only contain uppercase and lowercase letters, numbers, underscores (i.e. "\_"), and dashes (i.e. "-"). It's a safe bet to simply use the template's API identifier found at the bottom of the template's configuration page. Txbr itself places no restrictons on and performs no validation against this field, so it can contain any custom slug you want.
35
+
36
+ ### Translating Content
37
+
38
+ Strings can be inserted into the template using liquid tags. For example, the `{{strings.header.title | default: "Buy my stuff!}}` tag above defines and fetches a string at the key `header.title`, with a default English value of "Buy my stuff!" Txbr uses this key and default value to construct the translation file it will ultimately submit to Transifex. Providing a default value allows for easy template construction and previewing, since your template probably won't be translated immediately. Braze will fall back to this value if the translation doesn't yet exist. Liquid tags that do not specify a default value will not be included in the submission to Transifex. Finally, pay close attention to the `strings.` prefix. It corresponds to the `connected_content` tag's `:save strings` option. The `:save` option tells Braze to store the translated strings in a template variable called `strings`, which can then be used to grab individual strings. The value given to `:save` must be the same value used as the key prefix. For example, simply typing `{{header.title}}` won't work.
35
39
 
36
40
  ### Enabling Translation
37
41
 
@@ -41,6 +45,8 @@ Template translation is enabled by default. To skip translating a given template
41
45
  {% assign translation_enabled = false %}
42
46
  ```
43
47
 
48
+ **NOTE**: The `translation_enabled` variable assignment can be placed anywhere in the template, and affects the entire template. In other words, it does not just disable translations for content that comes after it. Its presence disables translations template-wide.
49
+
44
50
  Configuration
45
51
  ---
46
52
 
@@ -58,10 +64,10 @@ projects:
58
64
 
59
65
  ```
60
66
 
61
- 1. The `handler_id` indicates what kind of content this project should contain. In this case, we're translating email templates, the only currently supported option.
67
+ 1. The `handler_id` indicates what kind of content this project should contain. In this case, we're translating email templates. Campaigns are also supported via the handler ID `campaigns`, and are configured using the same options as email templates.
62
68
  2. Your Transifex username and password should have access to the Transifex projects you want to submit content to. You can configure access via Transifex's access control system.
63
69
  3. The `strings_format` option must be one of [Transifex's supported formats](https://docs.transifex.com/formats/introduction).
64
- 4. The `source_lang` option should be the language in which your source strings are written in. In other words, it should be the language in which the `default:` text is written in your template's Liquid tags.
70
+ 4. The `source_lang` option should be the language in which your source strings are written. In other words, it should be the language in which the `default:` text is written in your template's Liquid tags.
65
71
 
66
72
  ### Using Configuration
67
73
 
@@ -94,7 +100,7 @@ Txbr is both a library and a server. It provides access to Transifex resources v
94
100
 
95
101
  ### API Endpoint
96
102
 
97
- Txbr provides a single API endpoint for retrieving translated content from Transifex. You'll need to stand up a Txbr server somewhere and make it publicly available on the Internet. The URL to your server will be used in the `connected_content` tag in your Braze templates (see above).
103
+ Txbr provides a single API endpoint for retrieving translated content from Transifex. You'll need to stand up a Txbr server somewhere and make it publicly available on the Internet. The URL to your server should then be used in the `connected_content` tag in your Braze templates (see above).
98
104
 
99
105
  The endpoint will be available at http://your_txbr_server.com/strings.json and accepts the following required GET parameters:
100
106
 
@@ -3,14 +3,23 @@ require 'liquid'
3
3
  module Txbr
4
4
  autoload :Application, 'txbr/application'
5
5
  autoload :BrazeApi, 'txbr/braze_api'
6
+ autoload :Campaign, 'txbr/campaign'
7
+ autoload :CampaignHandler, 'txbr/campaign_handler'
8
+ autoload :CampaignsApi, 'txbr/campaigns_api'
9
+ autoload :ContentTag, 'txbr/content_tag'
6
10
  autoload :Commands, 'txbr/commands'
7
11
  autoload :Config, 'txbr/config'
8
12
  autoload :EmailTemplate, 'txbr/email_template'
9
13
  autoload :EmailTemplateComponent, 'txbr/email_template_component'
10
14
  autoload :EmailTemplateHandler, 'txbr/email_template_handler'
15
+ autoload :EmailTemplatesApi, 'txbr/email_templates_api'
16
+ autoload :Liquid, 'txbr/liquid'
17
+ autoload :Metadata, 'txbr/metadata'
11
18
  autoload :Project, 'txbr/project'
12
19
  autoload :RequestMethods, 'txbr/request_methods'
13
20
  autoload :StringsManifest, 'txbr/strings_manifest'
21
+ autoload :Template, 'txbr/template'
22
+ autoload :TemplateGroup, 'txbr/template_group'
14
23
  autoload :Uploader, 'txbr/uploader'
15
24
  autoload :Utils, 'txbr/utils'
16
25
 
@@ -52,28 +61,7 @@ module Txbr
52
61
  end
53
62
 
54
63
  Txbr.register_handler('email-templates', Txbr::EmailTemplateHandler)
64
+ Txbr.register_handler('campaigns', Txbr::CampaignHandler)
55
65
 
56
-
57
- class ConnectedContentTag < Liquid::Tag
58
- # This is a regular expression to pull out the variable in
59
- # which to store the value returned from the call made by
60
- # the connected_content filter. For example, if
61
- # connected_content makes a request to http://foo.com and is
62
- # told to store the results in a variable called "strings",
63
- # the API response will then be accessible via the normal
64
- # Liquid variable mechanism, i.e. {{...}}. Say the API at
65
- # foo.com returned something like {"bar":"baz"}, then the
66
- # template might contain {{strings.bar}}, which would print
67
- # out "baz".
68
- PREFIX_RE = /:save\s+(#{Liquid::Lexer::IDENTIFIER})/
69
-
70
- attr_reader :tag_name, :prefix
71
-
72
- def initialize(tag_name, arg, *)
73
- @tag_name = tag_name
74
- @prefix = arg.match(PREFIX_RE).captures.first
75
- end
76
- end
77
-
78
- Liquid::Template.register_tag(:connected_content, ConnectedContentTag)
66
+ ::Liquid::Template.register_tag(:connected_content, Txbr::Liquid::ConnectedContentTag)
79
67
  end
@@ -3,10 +3,6 @@ require 'faraday_middleware'
3
3
 
4
4
  module Txbr
5
5
  class BrazeApi
6
- TEMPLATE_BATCH_SIZE = 35
7
- TEMPLATE_LIST_PATH = 'templates/email/list'.freeze
8
- TEMPLATE_INFO_PATH = 'templates/email/info'.freeze
9
-
10
6
  include RequestMethods
11
7
 
12
8
  attr_reader :api_key, :api_url
@@ -17,24 +13,12 @@ module Txbr
17
13
  @connection = connection
18
14
  end
19
15
 
20
- def each_email_template(offset: 1, &block)
21
- return to_enum(__method__, offset: offset) unless block_given?
22
-
23
- loop do
24
- templates = get_json(
25
- TEMPLATE_LIST_PATH,
26
- offset: offset,
27
- limit: TEMPLATE_BATCH_SIZE
28
- )
29
-
30
- templates['templates'].each(&block)
31
- offset += templates['templates'].size
32
- break if templates['templates'].size < TEMPLATE_BATCH_SIZE
33
- end
16
+ def email_templates
17
+ @email_templates ||= EmailTemplatesApi.new(self)
34
18
  end
35
19
 
36
- def get_email_template_details(email_template_id:)
37
- get_json(TEMPLATE_INFO_PATH, email_template_id: email_template_id)
20
+ def campaigns
21
+ @campaigns ||= CampaignsApi.new(self)
38
22
  end
39
23
 
40
24
  private
@@ -0,0 +1,39 @@
1
+ require 'liquid'
2
+
3
+ module Txbr
4
+ class Campaign
5
+ attr_reader :project, :campaign_id
6
+
7
+ def initialize(project, campaign_id)
8
+ @project = project
9
+ @campaign_id = campaign_id
10
+ end
11
+
12
+ def each_resource(&block)
13
+ return to_enum(__method__) unless block_given?
14
+ template_group.each_resource(&block)
15
+ end
16
+
17
+ private
18
+
19
+ def template_group
20
+ @template_group ||= TemplateGroup.new(campaign_name, templates, project)
21
+ end
22
+
23
+ def templates
24
+ details['messages'].map do |message_id, props|
25
+ Txbr::Template.new(message_id, ::Liquid::Template.parse(props['message']))
26
+ end
27
+ end
28
+
29
+ def campaign_name
30
+ details['name']
31
+ end
32
+
33
+ def details
34
+ @details ||= project.braze_api.campaigns.details(
35
+ campaign_id: campaign_id
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,24 @@
1
+ module Txbr
2
+ class CampaignHandler
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_campaign { |campaign| campaign.each_resource(&block) }
12
+ end
13
+
14
+ # private
15
+
16
+ def each_campaign
17
+ return to_enum(__method__) unless block_given?
18
+
19
+ project.braze_api.campaigns.each do |campaign|
20
+ yield Campaign.new(project, campaign['id'])
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ module Txbr
2
+ class CampaignsApi
3
+ CAMPAIGN_BATCH_SIZE = 100 # from braze docs
4
+ CAMPAIGN_LIST_PATH = 'campaigns/list'.freeze
5
+ CAMPAIGN_DETAILS_PATH = 'campaigns/details'.freeze
6
+
7
+ attr_reader :braze_api
8
+
9
+ def initialize(braze_api)
10
+ @braze_api = braze_api
11
+ end
12
+
13
+ def each(&block)
14
+ return to_enum(__method__) unless block_given?
15
+ page = 0
16
+
17
+ loop do
18
+ campaigns = braze_api.get_json(CAMPAIGN_LIST_PATH, page: page)
19
+ campaigns['campaigns'].each(&block)
20
+ break if campaigns['campaigns'].size < CAMPAIGN_BATCH_SIZE
21
+ page += 1
22
+ end
23
+ end
24
+
25
+ def details(campaign_id:)
26
+ braze_api.get_json(CAMPAIGN_DETAILS_PATH, campaign_id: campaign_id)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,64 @@
1
+ module Txbr
2
+ class ContentTag
3
+ attr_reader :liquid_template, :liquid_tag
4
+
5
+ def initialize(liquid_template, liquid_tag)
6
+ @liquid_template = liquid_template
7
+ @liquid_tag = liquid_tag
8
+ end
9
+
10
+ def metadata
11
+ @metadata ||= begin
12
+ # Render the template to implicitly populate the metadata hash inside
13
+ # the liquid tag object. We do this because we encourage templates to
14
+ # set local variables for the project and resource slugs. It's less
15
+ # error-prone to let Liquid do the template evaluation instead of
16
+ # grepping through the nodelist looking for assignment statements.
17
+ liquid_template.render
18
+ liquid_tag.metadata
19
+ end
20
+ end
21
+
22
+ def strings_manifest
23
+ @strings_manifest ||= StringsManifest.new.tap do |manifest|
24
+ extract_strings_from(liquid_template.root, manifest)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def extract_strings_from(root, manifest)
31
+ return unless root.nodelist
32
+
33
+ root.nodelist.each do |node|
34
+ case node
35
+ # We only care about Liquid variables, which are written
36
+ # like {{prefix.foo.bar}}. We identify the prefix (i.e.
37
+ # the first lookup, or path segment) to verify it's
38
+ # associated with a connected_content call. Then we add
39
+ # the prefix and the rest of the lookups to the strings
40
+ # manifest along with the value. The prefix is used to
41
+ # divide the strings into individual Transifex resources
42
+ # while the rest of the lookups form the string's key.
43
+ when ::Liquid::Variable
44
+ next unless node.name.is_a?(::Liquid::VariableLookup)
45
+
46
+ string_prefix = node.name.name
47
+ path = node.name.lookups
48
+
49
+ # the English translation (or whatever language your
50
+ # source strings are written in) is provided using
51
+ # Liquid's built-in "default" filter
52
+ next unless string_prefix == metadata.prefix
53
+ default = node.filters.find { |f| f.first == 'default' }
54
+
55
+ manifest.add(path, default&.last&.first)
56
+ end
57
+
58
+ if node.respond_to?(:nodelist)
59
+ extract_strings_from(node, manifest)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -9,51 +9,20 @@ module Txbr
9
9
  @email_template_id = email_template_id
10
10
  end
11
11
 
12
- def each_resource
12
+ def each_resource(&block)
13
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
14
+ template_group.each_resource(&block)
38
15
  end
39
16
 
40
17
  private
41
18
 
42
- def strings_map
43
- @strings_map ||= %w(body 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?
19
+ def template_group
20
+ @template_group ||= TemplateGroup.new(template_name, templates, project)
21
+ end
51
22
 
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
23
+ def templates
24
+ %w(body subject preheader).map do |name|
25
+ Txbr::Template.new(::Liquid::Template.parse(details[name]))
57
26
  end
58
27
  end
59
28
 
@@ -62,7 +31,7 @@ module Txbr
62
31
  end
63
32
 
64
33
  def details
65
- @details ||= project.braze_api.get_email_template_details(
34
+ @details ||= project.braze_api.email_templates.details(
66
35
  email_template_id: email_template_id
67
36
  )
68
37
  end
@@ -16,7 +16,7 @@ module Txbr
16
16
  def each_template
17
17
  return to_enum(__method__) unless block_given?
18
18
 
19
- project.braze_api.each_email_template do |tmpl|
19
+ project.braze_api.email_templates.each do |tmpl|
20
20
  yield EmailTemplate.new(project, tmpl['email_template_id'])
21
21
  end
22
22
  end
@@ -0,0 +1,33 @@
1
+ module Txbr
2
+ class EmailTemplatesApi
3
+ TEMPLATE_BATCH_SIZE = 35
4
+ TEMPLATE_LIST_PATH = 'templates/email/list'.freeze
5
+ TEMPLATE_DETAILS_PATH = 'templates/email/info'.freeze
6
+
7
+ attr_reader :braze_api
8
+
9
+ def initialize(braze_api)
10
+ @braze_api = braze_api
11
+ end
12
+
13
+ def each(offset: 1, &block)
14
+ return to_enum(__method__, offset: offset) unless block_given?
15
+
16
+ loop do
17
+ templates = braze_api.get_json(
18
+ TEMPLATE_LIST_PATH,
19
+ offset: offset,
20
+ limit: TEMPLATE_BATCH_SIZE
21
+ )
22
+
23
+ templates['templates'].each(&block)
24
+ offset += templates['templates'].size
25
+ break if templates['templates'].size < TEMPLATE_BATCH_SIZE
26
+ end
27
+ end
28
+
29
+ def details(email_template_id:)
30
+ braze_api.get_json(TEMPLATE_DETAILS_PATH, email_template_id: email_template_id)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Txbr
2
+ module Liquid
3
+ autoload :ConnectedContentTag, 'txbr/liquid/connected_content_tag'
4
+ end
5
+ end