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 @@ - +