diff --git a/documentation/assets/formatter.graffle b/documentation/assets/formatter.graffle
index d9c327a4..52ca92da 100644
--- a/documentation/assets/formatter.graffle
+++ b/documentation/assets/formatter.graffle
@@ -172,7 +172,7 @@
\f0\fs36 \cf0 /**\
* Apple Strings File\
- * Generated by Twine 0.8.1\
+ * Generated by Twine\
* Language: en\
*/\
\
diff --git a/documentation/formatters.md b/documentation/formatters.md
index 5c8aa16e..2304b9a6 100644
--- a/documentation/formatters.md
+++ b/documentation/formatters.md
@@ -30,12 +30,6 @@ If we map the _input_ for each method to the example file, it looks like this
As stated at the beginning, the output produced by a formatter depends on the formatter. The output of the Apple formatter would for example be
```
-/**
- * Apple Strings File
- * Generated by Twine 0.8.1
- * Language: en
- */
-
/********** General **********/
"yes" = "Yes";
diff --git a/lib/twine.rb b/lib/twine.rb
index ab02f794..c08499b6 100644
--- a/lib/twine.rb
+++ b/lib/twine.rb
@@ -31,6 +31,7 @@ class Error < StandardError
require 'twine/formatters/abstract'
require 'twine/formatters/android'
require 'twine/formatters/apple'
+ require 'twine/formatters/apple_plural'
require 'twine/formatters/django'
require 'twine/formatters/flash'
require 'twine/formatters/gettext'
diff --git a/lib/twine/formatters/abstract.rb b/lib/twine/formatters/abstract.rb
index fd2114cd..ad70f3aa 100644
--- a/lib/twine/formatters/abstract.rb
+++ b/lib/twine/formatters/abstract.rb
@@ -3,6 +3,7 @@
module Twine
module Formatters
class Abstract
+ SUPPORTS_PLURAL = false
LANGUAGE_CODE_WITH_OPTIONAL_REGION_CODE = "[a-z]{2}(?:-[A-Za-z]{2})?"
attr_accessor :twine_file
@@ -139,7 +140,13 @@ def format_section(section, lang)
end
def format_definition(definition, lang)
- [format_comment(definition, lang), format_key_value(definition, lang)].compact.join
+ formatted_definition = [format_comment(definition, lang)]
+ if self.class::SUPPORTS_PLURAL && definition.is_plural?
+ formatted_definition << format_plural(definition, lang)
+ else
+ formatted_definition << format_key_value(definition, lang)
+ end
+ formatted_definition.compact.join
end
def format_comment(definition, lang)
@@ -150,10 +157,21 @@ def format_key_value(definition, lang)
key_value_pattern % { key: format_key(definition.key.dup), value: format_value(value.dup) }
end
+ def format_plural(definition, lang)
+ plural_hash = definition.plural_translation_for_lang(lang)
+ if plural_hash
+ format_plural_keys(definition.key.dup, plural_hash)
+ end
+ end
+
def key_value_pattern
raise NotImplementedError.new("You must implement key_value_pattern in your formatter class.")
end
+ def format_plural_keys(key, plural_hash)
+ raise NotImplementedError.new("You must implement format_plural_keys in your formatter class.")
+ end
+
def format_key(key)
key
end
diff --git a/lib/twine/formatters/android.rb b/lib/twine/formatters/android.rb
index 672e1e4e..ce613a69 100644
--- a/lib/twine/formatters/android.rb
+++ b/lib/twine/formatters/android.rb
@@ -7,6 +7,17 @@ module Formatters
class Android < Abstract
include Twine::Placeholders
+ SUPPORTS_PLURAL = true
+ LANG_CODES = Hash[
+ 'zh' => 'zh-Hans',
+ 'zh-CN' => 'zh-Hans',
+ 'zh-HK' => 'zh-Hant',
+ # See https://developer.android.com/reference/java/util/Locale#legacy-language-codes
+ 'iw' => 'he',
+ 'in' => 'id',
+ 'ji' => 'yi'
+ ]
+
def format_name
'android'
end
@@ -33,7 +44,10 @@ def determine_language_given_path(path)
# see http://developer.android.com/guide/topics/resources/providing-resources.html#AlternativeResources
match = /^values-([a-z]{2}(-r[a-z]{2})?)$/i.match(segment)
- return match[1].sub('-r', '-') if match
+ if match
+ lang = match[1].sub('-r', '-')
+ return LANG_CODES.fetch(lang, lang)
+ end
end
end
@@ -82,7 +96,7 @@ def read(io, lang)
end
def format_header(lang)
- "\n\n\n"
+ ""
end
def format_sections(twine_file, lang)
@@ -94,15 +108,25 @@ def format_sections(twine_file, lang)
end
def format_section_header(section)
- "\t"
+ "#{space(4)}"
end
def format_comment(definition, lang)
- "\t\n" if definition.comment
+ "#{space(4)}\n" if definition.comment
end
def key_value_pattern
- "\t%{value}"
+ "#{space(4)}%{value}"
+ end
+
+ def format_plural_keys(key, plural_hash)
+ result = "#{space(4)}\n"
+ result += plural_hash.map{|quantity,value| "#{space(8)}- #{escape_value(value)}
"}.join("\n")
+ result += "\n#{space(4)}"
+ end
+
+ def space(level)
+ ' ' * level
end
def gsub_unless(text, pattern, replacement)
@@ -136,6 +160,7 @@ def escape_value(value)
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)))/
end
value = gsub_unless(value, angle_bracket, '<') { |substring| substring =~ inside_cdata }
+ value = gsub_unless(value, '\n', "\n\\n") { |substring| substring =~ inside_cdata }
# escape non resource identifier @ signs (http://developer.android.com/guide/topics/resources/accessing-resources.html#ResourcesFromXml)
resource_identifier_regex = /@(?!([a-z\.]+:)?[a-z+]+\/[a-zA-Z_]+)/ # @[:]/
diff --git a/lib/twine/formatters/apple.rb b/lib/twine/formatters/apple.rb
index 920f7755..8aa8623c 100644
--- a/lib/twine/formatters/apple.rb
+++ b/lib/twine/formatters/apple.rb
@@ -1,6 +1,8 @@
module Twine
module Formatters
class Apple < Abstract
+ include Twine::Placeholders
+
def format_name
'apple'
end
@@ -63,10 +65,6 @@ def read(io, lang)
end
end
- def format_header(lang)
- "/**\n * Apple Strings File\n * Generated by Twine #{Twine::VERSION}\n * Language: #{lang}\n */"
- end
-
def format_section_header(section)
"/********** #{section.name} **********/\n"
end
@@ -84,8 +82,14 @@ def format_key(key)
end
def format_value(value)
+ # Replace Android's %s with iOS %@
+ value = convert_placeholders_from_android_to_twine(value)
escape_quotes(value)
end
+
+ def should_include_definition(definition, lang)
+ return !definition.is_plural? && super
+ end
end
end
end
diff --git a/lib/twine/formatters/apple_plural.rb b/lib/twine/formatters/apple_plural.rb
new file mode 100644
index 00000000..47a2db20
--- /dev/null
+++ b/lib/twine/formatters/apple_plural.rb
@@ -0,0 +1,72 @@
+module Twine
+ module Formatters
+ class ApplePlural < Apple
+ include Twine::Placeholders
+
+ SUPPORTS_PLURAL = true
+
+ def format_name
+ 'apple-plural'
+ end
+
+ def extension
+ '.stringsdict'
+ end
+
+ def default_file_name
+ 'Localizable.stringsdict'
+ end
+
+ def format_footer(lang)
+ footer = "\n"
+ end
+
+ def format_file(lang)
+ result = super
+ result += format_footer(lang)
+ end
+
+ def format_header(lang)
+ header = "<\?xml version=\"1.0\" encoding=\"UTF-8\"\?>\n"
+ header += "\n"
+ header += "\n"
+ end
+
+ def format_section_header(section)
+ "\n"
+ end
+
+ def format_plural_keys(key, plural_hash)
+ result = "\t#{key}\n"
+ result += "\t\n"
+ result += "\t\tNSStringLocalizedFormatKey\n"
+ result += "\t\t\%\#@value@\n"
+ result += "\t\tvalue\n"
+ result += "\t\t\n"
+ result += "\t\t\tNSStringFormatSpecTypeKey\n"
+ result += "\t\t\tNSStringPluralRuleType\n"
+ result += "\t\t\tNSStringFormatValueTypeKey\n"
+ result += "\t\t\td\n"
+ # Replace Android's %s with iOS %@
+ result += plural_hash.map{|quantity,value| "\t\t\t#{quantity}\n\t\t\t#{convert_placeholders_from_android_to_twine(value)}"}.join("\n")
+ result += "\n"
+ result += "\t\t\n"
+ result += "\t\n"
+ end
+
+ def format_comment(definition, lang)
+ "\n" if definition.comment
+ end
+
+ def read(io, lang)
+ raise NotImplementedError.new("Reading \".stringdict\" files not implemented yet")
+ end
+
+ def should_include_definition(definition, lang)
+ return definition.is_plural? && definition.plural_translation_for_lang(lang)
+ end
+ end
+ end
+end
+
+Twine::Formatters.formatters << Twine::Formatters::ApplePlural.new
diff --git a/lib/twine/formatters/django.rb b/lib/twine/formatters/django.rb
index d0bc5377..153b115d 100644
--- a/lib/twine/formatters/django.rb
+++ b/lib/twine/formatters/django.rb
@@ -55,7 +55,7 @@ def format_file(lang)
def format_header(lang)
# see https://www.gnu.org/software/trans-coord/manual/gnun/html_node/PO-Header.html for details
- "# Django Strings File\n# Generated by Twine #{Twine::VERSION}\n# Language: #{lang}\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\""
+ "# Django Strings File\n# Generated by Twine\n# Language: #{lang}\nmsgid \"\"\nmsgstr \"\"\n\"Content-Type: text/plain; charset=UTF-8\\n\""
end
def format_section_header(section)
diff --git a/lib/twine/formatters/flash.rb b/lib/twine/formatters/flash.rb
index 93858bb9..a07161f4 100644
--- a/lib/twine/formatters/flash.rb
+++ b/lib/twine/formatters/flash.rb
@@ -42,7 +42,7 @@ def format_sections(twine_file, lang)
end
def format_header(lang)
- "## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}"
+ "## Flash Strings File\n## Generated by Twine\n## Language: #{lang}"
end
def format_section_header(section)
diff --git a/lib/twine/formatters/jquery.rb b/lib/twine/formatters/jquery.rb
index 2712782f..cfbce5bc 100644
--- a/lib/twine/formatters/jquery.rb
+++ b/lib/twine/formatters/jquery.rb
@@ -14,7 +14,7 @@ def default_file_name
end
def determine_language_given_path(path)
- match = /^.+-([^-]{2})\.json$/.match File.basename(path)
+ match = /^.+([a-z]{2}-[A-Z]{2})\.json$/.match File.basename(path)
return match[1] if match
return super
diff --git a/lib/twine/formatters/tizen.rb b/lib/twine/formatters/tizen.rb
index 5df03c35..8ffd741b 100644
--- a/lib/twine/formatters/tizen.rb
+++ b/lib/twine/formatters/tizen.rb
@@ -91,7 +91,7 @@ def read(io, lang)
end
def format_header(lang)
- "\n\n\n"
+ "\n\n\n"
end
def format_sections(twine_file, lang)
diff --git a/lib/twine/output_processor.rb b/lib/twine/output_processor.rb
index 56f07478..4e9edf94 100644
--- a/lib/twine/output_processor.rb
+++ b/lib/twine/output_processor.rb
@@ -17,7 +17,12 @@ def fallback_languages(language)
'zh-TW' => 'zh-Hant' # if we don't have a zh-TW translation, try zh-Hant before en
}
- [fallback_mapping[language], default_language].flatten.compact
+ # Regional dialect fallbacks to generic language (for example: 'es-MX' to 'es' instead of default 'en').
+ if language.match(/([a-zA-Z]{2})-[a-zA-Z]+/)
+ generic_language = language.gsub(/([a-zA-Z])-[a-zA-Z]+/, '\1')
+ end
+
+ [fallback_mapping[language], generic_language, default_language].flatten.compact
end
def process(language)
@@ -43,6 +48,14 @@ def process(language)
new_definition = definition.dup
new_definition.translations[language] = value
+ if definition.is_plural?
+ # If definition is plural, but no translation found -> create
+ # Then check 'other' key
+ if !(new_definition.plural_translations[language] ||= {}).key? 'other'
+ new_definition.plural_translations[language]['other'] = value
+ end
+ end
+
new_section.definitions << new_definition
result.definitions_by_key[new_definition.key] = new_definition
end
diff --git a/lib/twine/twine_file.rb b/lib/twine/twine_file.rb
index 79ccc3d1..9d0a1e5e 100644
--- a/lib/twine/twine_file.rb
+++ b/lib/twine/twine_file.rb
@@ -1,9 +1,13 @@
module Twine
class TwineDefinition
+ PLURAL_KEYS = %w(zero one two few many other)
+
attr_reader :key
attr_accessor :comment
attr_accessor :tags
attr_reader :translations
+ attr_reader :plural_translations
+ attr_reader :is_plural
attr_accessor :reference
attr_accessor :reference_key
@@ -12,6 +16,7 @@ def initialize(key)
@comment = nil
@tags = nil
@translations = {}
+ @plural_translations = {}
end
def comment
@@ -50,6 +55,16 @@ def translation_for_lang(lang)
return translation
end
+
+ def plural_translation_for_lang(lang)
+ if @plural_translations.has_key? lang
+ @plural_translations[lang].dup.sort_by { |key,_| TwineDefinition::PLURAL_KEYS.index(key) }.to_h
+ end
+ end
+
+ def is_plural?
+ !@plural_translations.empty?
+ end
end
class TwineSection
@@ -137,11 +152,12 @@ def read(path)
parsed = true
end
else
- match = /^([^=]+)=(.*)$/.match(line)
+ match = /^([^:=]+)(?::([^=]+))?=(.*)$/.match(line)
if match
key = match[1].strip
- value = match[2].strip
-
+ plural_key = match[2].to_s.strip
+ value = match[3].strip
+
value = value[1..-2] if value[0] == '`' && value[-1] == '`'
case key
@@ -155,7 +171,18 @@ def read(path)
if !@language_codes.include? key
add_language_code(key)
end
- current_definition.translations[key] = value
+ # Providing backward compatibility
+ # for formatters without plural support
+ if plural_key.empty? || plural_key == 'other'
+ current_definition.translations[key] = value
+ end
+ if !plural_key.empty?
+ if !TwineDefinition::PLURAL_KEYS.include? plural_key
+ warn("Unknown plural key #{plural_key}")
+ next
+ end
+ (current_definition.plural_translations[key] ||= {})[plural_key] = value
+ end
end
parsed = true
end
@@ -186,25 +213,34 @@ def write(path)
f.puts "[[#{section.name}]]"
section.definitions.each do |definition|
- f.puts "\t[#{definition.key}]"
+ f.puts "\n#{space(2)}[#{definition.key}]"
- value = write_value(definition, dev_lang, f)
- if !value && !definition.reference_key
- Twine::stdout.puts "WARNING: #{definition.key} does not exist in developer language '#{dev_lang}'"
- end
-
- if definition.reference_key
- f.puts "\t\tref = #{definition.reference_key}"
+ if definition.raw_comment and definition.raw_comment.length > 0
+ f.puts "#{space(4)}comment = #{definition.raw_comment}"
end
if definition.tags && definition.tags.length > 0
tag_str = definition.tags.join(',')
- f.puts "\t\ttags = #{tag_str}"
+ f.puts "#{space(4)}tags = #{tag_str}"
end
- if definition.raw_comment and definition.raw_comment.length > 0
- f.puts "\t\tcomment = #{definition.raw_comment}"
+ if definition.reference_key
+ f.puts "#{space(4)}ref = #{definition.reference_key}"
+ end
+
+ value = write_value(definition, dev_lang, f)
+ if !value && !definition.reference_key
+ Twine::stdout.puts "WARNING: #{definition.key} does not exist in developer language '#{dev_lang}'"
end
+
@language_codes[1..-1].each do |lang|
- write_value(definition, lang, f)
+ if lang =~ /^#{dev_lang}/
+ write_value(definition, lang, f)
+ end
+ end
+
+ @language_codes[1..-1].each do |lang|
+ if not lang =~ /^#{dev_lang}/
+ write_value(definition, lang, f)
+ end
end
end
end
@@ -221,9 +257,14 @@ def write_value(definition, language, file)
value = '`' + value + '`'
end
- file.puts "\t\t#{language} = #{value}"
+ file.puts "#{space(4)}#{language} = #{value}"
return value
end
+ def space(level)
+ ' ' * level
+ end
+
+
end
end
diff --git a/lib/twine/version.rb b/lib/twine/version.rb
index 669bf16d..52096e33 100644
--- a/lib/twine/version.rb
+++ b/lib/twine/version.rb
@@ -1,3 +1,3 @@
module Twine
- VERSION = '1.1.2'
+ VERSION = '1.1.2-om'
end
diff --git a/test/fixtures/formatter_android.xml b/test/fixtures/formatter_android.xml
index 786557b5..7fcb655a 100644
--- a/test/fixtures/formatter_android.xml
+++ b/test/fixtures/formatter_android.xml
@@ -1,15 +1,12 @@
-
-
-
-
-
- value1-english
- value2-english
+
+
+ value1-english
+ value2-english
-
- value3-english
-
- value4-english
+
+ value3-english
+
+ value4-english
diff --git a/test/fixtures/formatter_apple.strings b/test/fixtures/formatter_apple.strings
index de00eb0d..5884aaea 100644
--- a/test/fixtures/formatter_apple.strings
+++ b/test/fixtures/formatter_apple.strings
@@ -1,9 +1,3 @@
-/**
- * Apple Strings File
- * Generated by Twine <%= Twine::VERSION %>
- * Language: en
- */
-
/********** Section 1 **********/
/* comment key1 */
diff --git a/test/fixtures/formatter_django.po b/test/fixtures/formatter_django.po
index 07506849..083bb41a 100644
--- a/test/fixtures/formatter_django.po
+++ b/test/fixtures/formatter_django.po
@@ -1,5 +1,5 @@
# Django Strings File
-# Generated by Twine <%= Twine::VERSION %>
+# Generated by Twine
# Language: en
msgid ""
msgstr ""
diff --git a/test/fixtures/formatter_flash.properties b/test/fixtures/formatter_flash.properties
index a2da7a76..0c0803c7 100644
--- a/test/fixtures/formatter_flash.properties
+++ b/test/fixtures/formatter_flash.properties
@@ -1,5 +1,5 @@
## Flash Strings File
-## Generated by Twine <%= Twine::VERSION %>
+## Generated by Twine
## Language: en
## Section 1 ##
diff --git a/test/fixtures/formatter_tizen.xml b/test/fixtures/formatter_tizen.xml
index 889d2c5a..63e7cbb5 100644
--- a/test/fixtures/formatter_tizen.xml
+++ b/test/fixtures/formatter_tizen.xml
@@ -1,6 +1,6 @@
-
+