twine 1.0.2 → 1.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 0e7971931654800b4d398dd6c3d9e86b96e4f5ba
4
- data.tar.gz: 64a1eccc6c604439e10c82bded869527b123f8db
2
+ SHA256:
3
+ metadata.gz: d8667f50ad7147b8b7edac0d82ce542c2ecc973604c8334e878513b6acaa9a14
4
+ data.tar.gz: 1ceb2b2b18dea8a3585bd91a1b427b30ebd9c888817dcf7140b3d211cd9e157c
5
5
  SHA512:
6
- metadata.gz: 808d4045076dfef7fcba6d26797eb05d7638fd3c2adc964c24059a20f378beb8c5d978f1598d651e1117a13a1fbdbdf102d21c767aee3a68c7eb5699fae7215a
7
- data.tar.gz: a63755c14cbadb14c0bba2960ff178a5785280bbb72e300dba5dc449ea510c823c6e248a1b848306f7c5de532c658ef8d4f034e80347f58ec2a4525673cf1ab6
6
+ metadata.gz: a961953ec7b74510c10e17114b84c227bac43117a869ba4254d7410a16ea9413e4133a2c9f099c3d67922fd6e1632d841f366e79d28687e61f73861721dea43a
7
+ data.tar.gz: 954b69b0d6e0fadec38cf145e0e1772eb1fde0259337b45ffac172d41b6f6c031b33373c768cfbb48ad3e9cfb1565dc9e91db8e825023421fdc30098439a4d86
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Twine
2
2
 
3
- [![Continuous Integration by CircleCI](https://circleci.com/gh/teespring/twine.svg?style=shield)](https://circleci.com/gh/teespring/twine)
3
+ [![Continuous Integration by CircleCI](https://circleci.com/gh/scelis/twine.svg?style=shield)](https://circleci.com/gh/scelis/twine)
4
4
 
5
5
  Twine is a command line tool for managing your strings and their translations. These are all stored in a master text file and then Twine uses this file to import and export localization files in a variety of types, including iOS and Mac OS X `.strings` files, Android `.xml` files, gettext `.po` files, and [jquery-localize][jquerylocalize] `.json` files. This allows individuals and companies to easily share translations across multiple projects, as well as export localization files in any format the user wants.
6
6
 
@@ -80,7 +80,7 @@ Twine currently supports the following output formats:
80
80
  * [Android String Resources][androidstrings] (format: android)
81
81
  * HTML tags will be escaped by replacing `<` with `&lt`
82
82
  * Tags inside `<![CDATA[` won't be escaped.
83
- * Supports [basic styling][androidstyling] with `<b>`, `<i>`, `<u>` and `<a>` links.
83
+ * Supports [basic styling][androidstyling] according to [Android documentation](https://developer.android.com/guide/topics/resources/string-resource.html#StylingWithHTML). All of the documented tags are supported, in addition to `<a>` links.
84
84
  * These tags will *not* be escaped if the string doesn't contain placeholders. You can reference them directly in your layouts or by using [`getText()`](https://developer.android.com/reference/android/content/res/Resources.html#getText(int)) to read them programatically.
85
85
  * These tags *will* be escaped if the string contains placeholders. You can use [`getString()`](https://developer.android.com/reference/android/content/res/Resources.html#getString(int,%20java.lang.Object...)) combined with [`fromHtml`](https://developer.android.com/reference/android/text/Html.html#fromHtml(java.lang.String)) as shown in the [documentation][androidstyling] to display them.
86
86
  * See [\#212](https://github.com/scelis/twine/issues/212) for details.
@@ -150,7 +150,7 @@ This command validates that the Twine data file can be parsed, contains no dupli
150
150
  The easiest way to create your first Twine data file is to run the [`consume-all-localization-files`](#consume-all-localization-files) command. The one caveat is to first create a blank file to use as your starting point. Then, just point the `consume-all-localization-files` command at a directory in your project containing all of your localization files.
151
151
 
152
152
  $ touch twine.txt
153
- $ twine consume-all-localization-files twine.txt Resources/Locales --developer-language en --consume-all --consume-comments
153
+ $ twine consume-all-localization-files twine.txt Resources/Locales --developer-language en --consume-all --consume-comments --format apple/android/gettext/jquery/django/tizen/flash
154
154
 
155
155
  ## Twine and Your Build Process
156
156
 
@@ -174,7 +174,10 @@ Now, whenever you build your application, Xcode will automatically invoke Twine
174
174
 
175
175
  ### Android Studio/Gradle
176
176
 
177
- Add the following task at the top level in app/build.gradle:
177
+ #### Standard
178
+
179
+ Add the following code to `app/build.gradle`:
180
+
178
181
  ```
179
182
  task generateLocalizations {
180
183
  String script = 'if hash twine 2>/dev/null; then twine generate-localization-file twine.txt ./src/main/res/values/generated_strings.xml; fi'
@@ -183,10 +186,46 @@ task generateLocalizations {
183
186
  args '-c', script
184
187
  }
185
188
  }
189
+
190
+ preBuild {
191
+ dependsOn generateLocalizations
192
+ }
186
193
  ```
187
194
 
188
- Now every time you build your app the localization files are generated from the Twine file.
195
+ #### Using [jruby](http://jruby.org)
189
196
 
197
+ With this approach, developers do not need to manually install ruby, gem, or twine.
198
+
199
+ Add the following code to `app/build.gradle`:
200
+
201
+ ```
202
+ buildscript {
203
+ repositories { jcenter() }
204
+
205
+ dependencies {
206
+ /* NOTE: Set your prefered version of jruby here. */
207
+ classpath "com.github.jruby-gradle:jruby-gradle-plugin:1.5.0"
208
+ }
209
+ }
210
+
211
+ apply plugin: 'com.github.jruby-gradle.base'
212
+
213
+ dependencies {
214
+ /* NOTE: Set your prefered version of twine here. */
215
+ jrubyExec 'rubygems:twine:1.0.3'
216
+ }
217
+
218
+ task generateLocalizations (type: JRubyExec) {
219
+ dependsOn jrubyPrepare
220
+ jrubyArgs '-S'
221
+ script "twine"
222
+ scriptArgs 'generate-localization-file', 'twine.txt', './src/main/res/values/generated_strings.xml'
223
+ }
224
+
225
+ preBuild {
226
+ dependsOn generateLocalizations
227
+ }
228
+ ```
190
229
 
191
230
  ## User Interface
192
231
 
@@ -224,4 +263,4 @@ Many thanks to all of the contributors to the Twine project, including:
224
263
  [djangopo]: https://docs.djangoproject.com/en/dev/topics/i18n/translation/
225
264
  [tizen]: https://developer.tizen.org/documentation/articles/localization
226
265
  [flash]: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/mx/resources/IResourceManager.html#getString()
227
- [printf]: https://en.wikipedia.org/wiki/Printf_format_string
266
+ [printf]: https://en.wikipedia.org/wiki/Printf_format_string
@@ -45,6 +45,14 @@ module Twine
45
45
  files are UTF-16 without BOM, you need to specify if it's UTF-16LE or UTF16-BE.
46
46
  DESC
47
47
  },
48
+ escape_all_tags: {
49
+ switch: ['--[no-]escape-all-tags'],
50
+ description: <<-DESC,
51
+ Always escape all HTML tags. By default the Android formatter will ONLY escape styling tags, if a
52
+ string also contains placeholders. This flag enforces that styling tags are escaped regardless of
53
+ placeholders.
54
+ DESC
55
+ },
48
56
  file_name: {
49
57
  switch: ['-n', '--file-name FILE_NAME'],
50
58
  description: 'This flag may be used to overwrite the default file name of the format.'
@@ -78,6 +86,10 @@ module Twine
78
86
  switch: ['-p', '--[no-]pedantic'],
79
87
  description: 'When validating a Twine file, perform additional checks that go beyond pure validity (like presence of tags).'
80
88
  },
89
+ quiet: {
90
+ switch: ['-q', '--[no-]quiet'],
91
+ description: 'Suppress all console output except error messages.'
92
+ },
81
93
  tags: {
82
94
  switch: ['-t', '--tags TAG1,TAG2,TAG3', Array],
83
95
  description: <<-DESC,
@@ -107,9 +119,11 @@ module Twine
107
119
  optional_options: [
108
120
  :developer_language,
109
121
  :encoding,
122
+ :escape_all_tags,
110
123
  :format,
111
124
  :include,
112
125
  :languages,
126
+ :quiet,
113
127
  :tags,
114
128
  :untagged,
115
129
  :validate
@@ -128,9 +142,11 @@ module Twine
128
142
  :create_folders,
129
143
  :developer_language,
130
144
  :encoding,
145
+ :escape_all_tags,
131
146
  :file_name,
132
147
  :format,
133
148
  :include,
149
+ :quiet,
134
150
  :tags,
135
151
  :untagged,
136
152
  :validate
@@ -146,7 +162,9 @@ module Twine
146
162
  optional_options: [
147
163
  :developer_language,
148
164
  :encoding,
165
+ :escape_all_tags,
149
166
  :include,
167
+ :quiet,
150
168
  :tags,
151
169
  :untagged,
152
170
  :validate
@@ -164,6 +182,7 @@ module Twine
164
182
  :format,
165
183
  :languages,
166
184
  :output_path,
185
+ :quiet,
167
186
  :tags
168
187
  ],
169
188
  option_validation: Proc.new { |options|
@@ -183,6 +202,7 @@ module Twine
183
202
  :encoding,
184
203
  :format,
185
204
  :output_path,
205
+ :quiet,
186
206
  :tags
187
207
  ],
188
208
  example: 'twine consume-all-localization-files twine.txt Resources/Locales/ --developer-language en --tags DefaultTag1,DefaultTag2'
@@ -197,6 +217,7 @@ module Twine
197
217
  :encoding,
198
218
  :format,
199
219
  :output_path,
220
+ :quiet,
200
221
  :tags
201
222
  ],
202
223
  example: 'twine consume-localization-archive twine.txt LocDrop5.zip'
@@ -206,7 +227,8 @@ module Twine
206
227
  arguments: [:twine_file],
207
228
  optional_options: [
208
229
  :developer_language,
209
- :pedantic
230
+ :pedantic,
231
+ :quiet
210
232
  ],
211
233
  example: 'twine validate-twine-file twine.txt'
212
234
  }
@@ -227,7 +249,7 @@ module Twine
227
249
 
228
250
  mapped_command = DEPRECATED_COMMAND_MAPPINGS[command]
229
251
  if mapped_command
230
- Twine::stderr.puts "WARNING: Twine commands names have changed. `#{command}` is now `#{mapped_command}`. The old command is deprecated and will soon stop working. For more information please check the documentation at https://github.com/mobiata/twine"
252
+ Twine::stdout.puts "WARNING: Twine commands names have changed. `#{command}` is now `#{mapped_command}`. The old command is deprecated and will soon stop working. For more information please check the documentation at https://github.com/mobiata/twine"
231
253
  command = mapped_command
232
254
  end
233
255
 
@@ -3,6 +3,8 @@ require 'fileutils'
3
3
  module Twine
4
4
  module Formatters
5
5
  class Abstract
6
+ LANGUAGE_CODE_WITH_OPTIONAL_REGION_CODE = "[a-z]{2}(?:-[A-Za-z]{2})?"
7
+
6
8
  attr_accessor :twine_file
7
9
  attr_accessor :options
8
10
 
@@ -38,7 +40,7 @@ module Twine
38
40
  definition.translations[lang] = value
39
41
  end
40
42
  elsif @options[:consume_all]
41
- Twine::stderr.puts "Adding new definition '#{key}' to twine file."
43
+ Twine::stdout.puts "Adding new definition '#{key}' to twine file."
42
44
  current_section = @twine_file.sections.find { |s| s.name == 'Uncategorized' }
43
45
  unless current_section
44
46
  current_section = TwineSection.new('Uncategorized')
@@ -54,7 +56,7 @@ module Twine
54
56
  @twine_file.definitions_by_key[key] = current_definition
55
57
  @twine_file.definitions_by_key[key].translations[lang] = value
56
58
  else
57
- Twine::stderr.puts "Warning: '#{key}' not found in twine file."
59
+ Twine::stdout.puts "WARNING: '#{key}' not found in twine file."
58
60
  end
59
61
  if !@twine_file.language_codes.include?(lang)
60
62
  @twine_file.add_language_code(lang)
@@ -76,7 +78,12 @@ module Twine
76
78
  end
77
79
 
78
80
  def determine_language_given_path(path)
79
- raise NotImplementedError.new("You must implement determine_language_given_path in your formatter class.")
81
+ only_language_and_region = /^#{LANGUAGE_CODE_WITH_OPTIONAL_REGION_CODE}$/i
82
+ basename = File.basename(path, File.extname(path))
83
+ return basename if basename =~ only_language_and_region
84
+ return basename if @twine_file.language_codes.include? basename
85
+
86
+ path.split(File::SEPARATOR).reverse.find { |segment| segment =~ only_language_and_region }
80
87
  end
81
88
 
82
89
  def output_path_for_language(lang)
@@ -37,11 +37,15 @@ module Twine
37
37
  end
38
38
  end
39
39
 
40
- return
40
+ return super
41
41
  end
42
42
 
43
43
  def output_path_for_language(lang)
44
- "values-#{lang}".gsub(/-(\p{Lu})/, '-r\1')
44
+ if lang == @twine_file.language_codes[0]
45
+ "values"
46
+ else
47
+ "values-#{lang}".gsub(/-(\p{Lu})/, '-r\1')
48
+ end
45
49
  end
46
50
 
47
51
  def set_translation_for_key(key, lang, value)
@@ -56,7 +60,7 @@ module Twine
56
60
 
57
61
  def read(io, lang)
58
62
  document = REXML::Document.new io, :compress_whitespace => %w{ string }
59
-
63
+ document.context[:attribute_quote] = :quote
60
64
  comment = nil
61
65
  document.root.children.each do |child|
62
66
  if child.is_a? REXML::Comment
@@ -67,7 +71,8 @@ module Twine
67
71
 
68
72
  key = child.attributes['name']
69
73
 
70
- set_translation_for_key(key, lang, child.text)
74
+ content = child.children.map(&:to_s).join
75
+ set_translation_for_key(key, lang, content)
71
76
  set_comment_for_key(key, comment) if comment
72
77
 
73
78
  comment = nil
@@ -108,22 +113,26 @@ module Twine
108
113
 
109
114
  # http://developer.android.com/guide/topics/resources/string-resource.html#FormattingAndStyling
110
115
  def escape_value(value)
111
- inside_cdata = /<\!\[CDATA\[((?!\]\]>).)*$/ # opening CDATA tag ('<![CDATA[') not followed by a closing tag (']]>')
112
- inside_opening_anchor_tag = /<a\s?((?!>).)*$/ # anchor tag start ('<a ') not followed by a '>'
116
+ inside_cdata = /<\!\[CDATA\[((?!\]\]>).)*$/ # opening CDATA tag ('<![CDATA[') not followed by a closing tag (']]>')
117
+ inside_opening_tag = /<(a|font|span|p)\s?((?!>).)*$/ # tag start ('<a ', '<font ', '<span ' or '<p ') not followed by a '>'
113
118
 
114
119
  # escape double and single quotes and & signs
115
- value = gsub_unless(value, '"', '\\"') { |substring| substring =~ inside_cdata || substring =~ inside_opening_anchor_tag }
120
+ value = gsub_unless(value, '"', '\\"') { |substring| substring =~ inside_cdata || substring =~ inside_opening_tag }
116
121
  value = gsub_unless(value, "'", "\\'") { |substring| substring =~ inside_cdata }
117
- value = gsub_unless(value, /&/, '&amp;') { |substring| substring =~ inside_cdata || substring =~ inside_opening_anchor_tag }
122
+ value = gsub_unless(value, /&/, '&amp;') { |substring| substring =~ inside_cdata || substring =~ inside_opening_tag }
118
123
 
119
124
  # if `value` contains a placeholder, escape all angle brackets
120
125
  # if not, escape opening angle brackes unless it's a supported styling tag
121
126
  # https://github.com/scelis/twine/issues/212
122
127
  # https://stackoverflow.com/questions/3235131/#18199543
123
- if number_of_twine_placeholders(value) > 0
124
- angle_bracket = /<(?!(\/?(\!\[CDATA)))/ # matches all `<` but <![CDATA
125
- else
126
- angle_bracket = /<(?!(\/?(b|u|i|a|\!\[CDATA)))/ # matches all `<` but <b>, <u>, <i>, <a> and <![CDATA
128
+ if number_of_twine_placeholders(value) > 0 or @options[:escape_all_tags]
129
+ # matches all `<` but <![CDATA
130
+ angle_bracket = /<(?!(\/?(\!\[CDATA)))/
131
+ else
132
+ # matches all '<' but <b>, <em>, <i>, <cite>, <dfn>, <big>, <small>, <font>, <tt>, <s>,
133
+ # <strike>, <del>, <u>, <super>, <sub>, <ul>, <li>, <br>, <div>, <span>, <p>, <a>
134
+ # and <![CDATA
135
+ angle_bracket = /<(?!(\/?(b|em|i|cite|dfn|big|small|font|tt|s|strike|del|u|super|sub|ul|li|br|div|span|p|a|\!\[CDATA)))/
127
136
  end
128
137
  value = gsub_unless(value, angle_bracket, '&lt;') { |substring| substring =~ inside_cdata }
129
138
 
@@ -30,7 +30,7 @@ module Twine
30
30
  end
31
31
  end
32
32
 
33
- return
33
+ return super
34
34
  end
35
35
 
36
36
  def output_path_for_language(lang)
@@ -1,5 +1,6 @@
1
1
  module Twine
2
2
  module Formatters
3
+ # For a description of the .po file format, see https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
3
4
  class Django < Abstract
4
5
  def format_name
5
6
  'django'
@@ -13,22 +14,11 @@ module Twine
13
14
  'strings.po'
14
15
  end
15
16
 
16
- def determine_language_given_path(path)
17
- path_arr = path.split(File::SEPARATOR)
18
- path_arr.each do |segment|
19
- match = /([a-z]{2}(-[A-Za-z]{2})?)\.po$/.match(segment)
20
- return match[1] if match
21
- end
22
-
23
- return
24
- end
25
-
26
17
  def read(io, lang)
27
- comment_regex = /#\. *"?(.*)"?$/
28
- key_regex = /msgid *"(.*)"$/
29
- value_regex = /msgstr *"(.*)"$/m
18
+ comment_regex = /^\s*#\. *"?(.*)"?$/
19
+ key_regex = /^msgid *"(.*)"$/
20
+ value_regex = /^msgstr *"(.*)"$/m
30
21
 
31
- last_comment = nil
32
22
  while line = io.gets
33
23
  comment_match = comment_regex.match(line)
34
24
  if comment_match
@@ -64,11 +54,12 @@ module Twine
64
54
  end
65
55
 
66
56
  def format_header(lang)
67
- "##\n # Django Strings File\n # Generated by Twine #{Twine::VERSION}\n # Language: #{lang}\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\""
57
+ # see https://www.gnu.org/software/trans-coord/manual/gnun/html_node/PO-Header.html for details
58
+ "# Django Strings File\n# Generated by Twine #{Twine::VERSION}\n# Language: #{lang}\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\""
68
59
  end
69
60
 
70
61
  def format_section_header(section)
71
- "#--------- #{section.name} ---------#\n"
62
+ "# --------- #{section.name} --------- #\n"
72
63
  end
73
64
 
74
65
  def format_definition(definition, lang)
@@ -15,11 +15,6 @@ module Twine
15
15
  'resources.properties'
16
16
  end
17
17
 
18
- def determine_language_given_path(path)
19
- # match two-letter language code, optionally followed by a two letter region code
20
- path.split(File::SEPARATOR).reverse.find { |segment| segment =~ /^([a-z]{2}(-[a-z]{2})?)$/i }
21
- end
22
-
23
18
  def set_translation_for_key(key, lang, value)
24
19
  value = convert_placeholders_from_flash_to_twine(value)
25
20
  super(key, lang, value)
@@ -15,16 +15,6 @@ module Twine
15
15
  'strings.po'
16
16
  end
17
17
 
18
- def determine_language_given_path(path)
19
- path_arr = path.split(File::SEPARATOR)
20
- path_arr.each do |segment|
21
- match = /([a-z]{2}(-[A-Za-z]{2})?)\.po$/.match(segment)
22
- return match[1] if match
23
- end
24
-
25
- return
26
- end
27
-
28
18
  def read(io, lang)
29
19
  comment_regex = /#.? *"(.*)"$/
30
20
  key_regex = /msgctxt *"(.*)"$/
@@ -65,7 +55,7 @@ module Twine
65
55
  end
66
56
 
67
57
  def format_header(lang)
68
- "msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\\n\"\n\"X-Generator: Twine #{Twine::VERSION}\\n\"\n"
58
+ "msgid \"\"\nmsgstr \"\"\n\"Language: #{lang}\"\n\"X-Generator: Twine #{Twine::VERSION}\"\n"
69
59
  end
70
60
 
71
61
  def format_section_header(section)
@@ -86,15 +76,15 @@ module Twine
86
76
  end
87
77
 
88
78
  def format_key(key)
89
- "msgctxt \"#{key}\"\n"
79
+ "msgctxt \"#{escape_quotes(key)}\"\n"
90
80
  end
91
81
 
92
82
  def format_base_translation(definition)
93
- "msgid \"#{definition.translations[@default_lang]}\"\n"
83
+ "msgid \"#{escape_quotes(definition.translations[@default_lang])}\"\n"
94
84
  end
95
85
 
96
86
  def format_value(value)
97
- "msgstr \"#{value}\"\n"
87
+ "msgstr \"#{escape_quotes(value)}\"\n"
98
88
  end
99
89
  end
100
90
  end
@@ -14,22 +14,17 @@ module Twine
14
14
  end
15
15
 
16
16
  def determine_language_given_path(path)
17
- path_arr = path.split(File::SEPARATOR)
18
- path_arr.each do |segment|
19
- match = /^((.+)-)?([^-]+)\.json$/.match(segment)
20
- if match
21
- return match[3]
22
- end
23
- end
17
+ match = /^.+-([^-]{2})\.json$/.match File.basename(path)
18
+ return match[1] if match
24
19
 
25
- return
20
+ return super
26
21
  end
27
22
 
28
23
  def read(io, lang)
29
24
  begin
30
25
  require "json"
31
26
  rescue LoadError
32
- raise Twine::Error.new "You must run 'gem install json' in order to read or write jquery-localize files."
27
+ raise Twine::Error.new "You must run `gem install json` in order to read or write jquery-localize files."
33
28
  end
34
29
 
35
30
  json = JSON.load(io)
@@ -46,7 +41,7 @@ module Twine
46
41
 
47
42
  def format_sections(twine_file, lang)
48
43
  sections = twine_file.sections.map { |section| format_section(section, lang) }
49
- sections.delete_if &:empty?
44
+ sections.delete_if(&:empty?)
50
45
  sections.join(",\n\n")
51
46
  end
52
47