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 +4 -4
- data/README.md +16 -10
- data/lib/txbr.rb +11 -23
- data/lib/txbr/braze_api.rb +4 -20
- data/lib/txbr/campaign.rb +39 -0
- data/lib/txbr/campaign_handler.rb +24 -0
- data/lib/txbr/campaigns_api.rb +29 -0
- data/lib/txbr/content_tag.rb +64 -0
- data/lib/txbr/email_template.rb +9 -40
- data/lib/txbr/email_template_handler.rb +1 -1
- data/lib/txbr/email_templates_api.rb +33 -0
- data/lib/txbr/liquid.rb +5 -0
- data/lib/txbr/liquid/connected_content_tag.rb +99 -0
- data/lib/txbr/metadata.rb +30 -0
- data/lib/txbr/request_methods.rb +0 -2
- data/lib/txbr/template.rb +54 -0
- data/lib/txbr/template_group.rb +50 -0
- data/lib/txbr/version.rb +1 -1
- data/spec/campaign_spec.rb +136 -0
- data/spec/campaigns_api_spec.rb +71 -0
- data/spec/email_template_spec.rb +25 -10
- data/spec/{braze_api_spec.rb → email_templates_api_spec.rb} +11 -48
- data/spec/shared_examples/api_errors.rb +40 -0
- data/spec/uploader_spec.rb +10 -4
- metadata +16 -4
- data/lib/txbr/email_template_component.rb +0 -96
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d1499776cb6db1539a2d2701bfddd4e182f9b3246b28d13e245e97426002902
|
4
|
+
data.tar.gz: 4b4ad8fb50db7bc76fa87e0435df0de19e0d6809dc215584eb0e8b403de97b5c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
28
|
+
### The `connected_content` Tag
|
29
29
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
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
|
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
|
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
|
|
data/lib/txbr.rb
CHANGED
@@ -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
|
data/lib/txbr/braze_api.rb
CHANGED
@@ -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
|
21
|
-
|
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
|
37
|
-
|
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
|
data/lib/txbr/email_template.rb
CHANGED
@@ -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
|
43
|
-
@
|
44
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
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.
|
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.
|
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
|