Merge pull request #129 from sebastianludwig/formatter_refactoring

Formatter refactoring
This commit is contained in:
Sebastian Celis 2016-01-19 16:36:18 -06:00
commit 662da698ad
17 changed files with 206 additions and 163 deletions

View file

@ -23,11 +23,11 @@ module Twine
require 'twine/plugin'
require 'twine/cli'
require 'twine/stringsfile'
require 'twine/encoding'
require 'twine/output_processor'
require 'twine/placeholders'
require 'twine/formatters'
require 'twine/runner'
require 'twine/stringsfile'
require 'twine/version'
end

View file

@ -46,7 +46,7 @@ module Twine
opts.on('-u', '--untagged', 'If you have specified tags using the --tags flag, then only those tags will be selected. If you also want to select all strings that are untagged, then you can specify this option to do so.') do |u|
options[:untagged] = true
end
formats = Formatters.formatters.map { |f| f::FORMAT_NAME }
formats = Formatters.formatters.map(&:format_name)
opts.on('-f', '--format FORMAT', "The file format to read or write (#{formats.join(', ')}). Additional formatters can be placed in the formats/ directory.") do |format|
unless formats.include?(format.downcase)
raise Twine::Error.new "Invalid format: #{format}"

View file

@ -1,15 +1,6 @@
require 'twine/formatters/abstract'
require 'twine/formatters/android'
require 'twine/formatters/apple'
require 'twine/formatters/flash'
require 'twine/formatters/gettext'
require 'twine/formatters/jquery'
require 'twine/formatters/django'
require 'twine/formatters/tizen'
module Twine
module Formatters
@formatters = [Formatters::Apple, Formatters::Android, Formatters::Gettext, Formatters::JQuery, Formatters::Flash, Formatters::Django, Formatters::Tizen]
@formatters = []
class << self
attr_reader :formatters
@ -22,9 +13,12 @@ module Twine
# returns array of active formatters
#
def register_formatter formatter_class
raise "#{formatter_class} already registered" if @formatters.include? formatter_class
@formatters << formatter_class
@formatters << formatter_class.new
end
end
end
end
end
Dir[File.join(File.dirname(__FILE__), 'formatters', '*.rb')].each do |file|
require file
end

View file

@ -3,17 +3,28 @@ require 'fileutils'
module Twine
module Formatters
class Abstract
attr_reader :strings
attr_reader :options
attr_accessor :strings
attr_accessor :options
def self.can_handle_directory?(path)
return false
def initialize
@strings = StringsFile.new
@options = {}
end
def initialize(strings, options)
@strings = strings
@options = options
@output_processor = Processors::OutputProcessor.new @strings, @options
def format_name
raise NotImplementedError.new("You must implement format_name in your formatter class.")
end
def extension
raise NotImplementedError.new("You must implement extension in your formatter class.")
end
def can_handle_directory?(path)
raise NotImplementedError.new("You must implement can_handle_directory? in your formatter class.")
end
def default_file_name
raise NotImplementedError.new("You must implement default_file_name in your formatter class.")
end
def set_translation_for_key(key, lang, value)
@ -64,10 +75,6 @@ module Twine
end
end
def default_file_name
raise NotImplementedError.new("You must implement default_file_name in your formatter class.")
end
def determine_language_given_path(path)
raise NotImplementedError.new("You must implement determine_language_given_path in your formatter class.")
end
@ -92,21 +99,25 @@ module Twine
def format_sections(strings, lang)
sections = strings.sections.map { |section| format_section(section, lang) }
sections.join("\n")
sections.compact.join("\n")
end
def format_section_header(section)
end
def should_include_row(row, lang)
row.translated_string_for_lang(lang)
end
def format_section(section, lang)
rows = section.rows.dup
rows = section.rows.select { |row| should_include_row(row, lang) }
return if rows.empty?
result = ""
unless rows.empty?
if section.name && section.name.length > 0
section_header = format_section_header(section)
result += "\n#{section_header}" if section_header
end
if section.name && section.name.length > 0
section_header = format_section_header(section)
result += "\n#{section_header}" if section_header
end
rows.map! { |row| format_row(row, lang) }
@ -115,16 +126,8 @@ module Twine
result += rows.join
end
def row_pattern
"%{comment}%{key_value}"
end
def format_row(row, lang)
return nil unless row.translated_string_for_lang(lang)
result = row_pattern.scan(/%\{([a-z_]+)\}/).flatten
result.map! { |element| send("format_#{element}".to_sym, row, lang) }
result.flatten.join
[format_comment(row, lang), format_key_value(row, lang)].compact.join
end
def format_comment(row, lang)
@ -152,10 +155,10 @@ module Twine
end
def write_file(path, lang)
output_processor = Processors::OutputProcessor.new(@strings, @options)
processed_strings = output_processor.process(lang)
encoding = @options[:output_encoding] || 'UTF-8'
processed_strings = @output_processor.process(lang)
File.open(path, "w:#{encoding}") do |f|
f.puts format_file(processed_strings, lang)
end
@ -169,7 +172,8 @@ module Twine
FileUtils.mkdir_p(output_path)
write_file(File.join(output_path, file_name), lang)
file_path = File.join(output_path, file_name)
write_file(file_path, lang)
end
else
language_written = false
@ -182,7 +186,8 @@ module Twine
lang = determine_language_given_path(item)
next unless lang
write_file(File.join(item, file_name), lang)
file_path = File.join(item, file_name)
write_file(file_path, lang)
language_written = true
end

View file

@ -7,9 +7,6 @@ module Twine
class Android < Abstract
include Twine::Placeholders
FORMAT_NAME = 'android'
EXTENSION = '.xml'
DEFAULT_FILE_NAME = 'strings.xml'
LANG_CODES = Hash[
'zh' => 'zh-Hans',
'zh-rCN' => 'zh-Hans',
@ -20,12 +17,20 @@ module Twine
# TODO: spanish
]
def self.can_handle_directory?(path)
def format_name
'android'
end
def extension
'.xml'
end
def can_handle_directory?(path)
Dir.entries(path).any? { |item| /^values.*$/.match(item) }
end
def default_file_name
return DEFAULT_FILE_NAME
return 'strings.xml'
end
def determine_language_given_path(path)
@ -135,3 +140,5 @@ module Twine
end
end
end
Twine::Formatters.formatters << Twine::Formatters::Android.new

View file

@ -1,16 +1,20 @@
module Twine
module Formatters
class Apple < Abstract
FORMAT_NAME = 'apple'
EXTENSION = '.strings'
DEFAULT_FILE_NAME = 'Localizable.strings'
def format_name
'apple'
end
def self.can_handle_directory?(path)
def extension
'.strings'
end
def can_handle_directory?(path)
Dir.entries(path).any? { |item| /^.+\.lproj$/.match(item) }
end
def default_file_name
return DEFAULT_FILE_NAME
return 'Localizable.strings'
end
def determine_language_given_path(path)
@ -111,3 +115,5 @@ module Twine
end
end
end
Twine::Formatters.formatters << Twine::Formatters::Apple.new

View file

@ -1,16 +1,20 @@
module Twine
module Formatters
class Django < Abstract
FORMAT_NAME = 'django'
EXTENSION = '.po'
DEFAULT_FILE_NAME = 'strings.po'
def format_name
'django'
end
def self.can_handle_directory?(path)
Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
def extension
'.po'
end
def can_handle_directory?(path)
Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
end
def default_file_name
return DEFAULT_FILE_NAME
return 'strings.po'
end
def determine_language_given_path(path)
@ -92,7 +96,9 @@ module Twine
def format_file(strings, lang)
@default_lang = strings.language_codes[0]
super
result = super
@default_lang = nil
result
end
def format_header(lang)
@ -103,11 +109,11 @@ module Twine
"#--------- #{section.name} ---------#\n"
end
def row_pattern
"%{comment}%{base_translation}%{key_value}"
def format_row(row, lang)
[format_comment(row, lang), format_base_translation(row), format_key_value(row, lang)].compact.join
end
def format_base_translation(row, lang)
def format_base_translation(row)
base_translation = row.translations[@default_lang]
"# base translation: \"#{base_translation}\"\n" if base_translation
end
@ -131,3 +137,5 @@ module Twine
end
end
end
Twine::Formatters.formatters << Twine::Formatters::Django.new

View file

@ -1,16 +1,20 @@
module Twine
module Formatters
class Flash < Abstract
FORMAT_NAME = 'flash'
EXTENSION = '.properties'
DEFAULT_FILE_NAME = 'resources.properties'
def format_name
'flash'
end
def self.can_handle_directory?(path)
def extension
'.properties'
end
def can_handle_directory?(path)
return false
end
def default_file_name
return DEFAULT_FILE_NAME
return 'resources.properties'
end
def determine_language_given_path(path)
@ -93,3 +97,5 @@ module Twine
end
end
end
Twine::Formatters.formatters << Twine::Formatters::Flash.new

View file

@ -3,16 +3,20 @@
module Twine
module Formatters
class Gettext < Abstract
FORMAT_NAME = 'gettext'
EXTENSION = '.po'
DEFAULT_FILE_NAME = 'strings.po'
def format_name
'gettext'
end
def self.can_handle_directory?(path)
def extension
'.po'
end
def can_handle_directory?(path)
Dir.entries(path).any? { |item| /^.+\.po$/.match(item) }
end
def default_file_name
return DEFAULT_FILE_NAME
return 'strings.po'
end
def determine_language_given_path(path)
@ -62,7 +66,9 @@ module Twine
def format_file(strings, lang)
@default_lang = strings.language_codes[0]
super
result = super
@default_lang = nil
result
end
def format_header(lang)
@ -73,31 +79,32 @@ module Twine
"# SECTION: #{section.name}"
end
def row_pattern
"%{comment}%{key}%{base_translation}%{value}"
end
def format_row(row, lang)
return nil unless row.translated_string_for_lang(@default_lang)
super
def should_include_row(row, lang)
super and row.translated_string_for_lang(@default_lang)
end
def format_comment(row, lang)
"#. \"#{escape_quotes(row.comment)}\"\n" if row.comment
end
def format_key(row, lang)
"msgctxt \"#{row.key.dup}\"\n"
def format_key_value(row, lang)
value = row.translated_string_for_lang(lang)
[format_key(row.key.dup), format_base_translation(row), format_value(value.dup)].compact.join
end
def format_base_translation(row, lang)
def format_key(key)
"msgctxt \"#{key}\"\n"
end
def format_base_translation(row)
"msgid \"#{row.translations[@default_lang]}\"\n"
end
def format_value(row, lang)
"msgstr \"#{row.translated_string_for_lang(lang)}\"\n"
def format_value(value)
"msgstr \"#{value}\"\n"
end
end
end
end
Twine::Formatters.formatters << Twine::Formatters::Gettext.new

View file

@ -1,16 +1,20 @@
module Twine
module Formatters
class JQuery < Abstract
FORMAT_NAME = 'jquery'
EXTENSION = '.json'
DEFAULT_FILE_NAME = 'localize.json'
def format_name
'jquery'
end
def self.can_handle_directory?(path)
def extension
'.json'
end
def can_handle_directory?(path)
Dir.entries(path).any? { |item| /^.+\.json$/.match(item) }
end
def default_file_name
return DEFAULT_FILE_NAME
return 'localize.json'
end
def determine_language_given_path(path)
@ -71,15 +75,8 @@ module Twine
def format_value(value)
escape_quotes(value)
end
def write_file(path, lang)
begin
require "json"
rescue LoadError
raise Twine::Error.new "You must run 'gem install json' in order to read or write jquery-localize files."
end
super
end
end
end
end
Twine::Formatters.formatters << Twine::Formatters::JQuery.new

View file

@ -7,9 +7,6 @@ module Twine
class Tizen < Abstract
include Twine::Placeholders
FORMAT_NAME = 'tizen'
EXTENSION = '.xml'
DEFAULT_FILE_NAME = 'strings.xml'
LANG_CODES = Hash[
'eng-GB' => 'en',
'rus-RU' => 'ru',
@ -22,39 +19,21 @@ module Twine
'por-PT' => 'pt',
'ukr-UA' => 'uk'
]
DEFAULT_LANG_CODES = Hash[
]
def self.can_handle_directory?(path)
def format_name
'tizen'
end
def extension
'.xml'
end
def can_handle_directory?(path)
Dir.entries(path).any? { |item| /^values.*$/.match(item) }
end
def default_file_name
return DEFAULT_FILE_NAME
end
def write_all_files(path)
if !File.directory?(path)
raise Twine::Error.new("Directory does not exist: #{path}")
end
langs_written = []
Dir.foreach(path) do |item|
if item == "." or item == ".."
next
end
item = File.join(path, item)
if !File.directory?(item)
lang = determine_language_given_path(item)
if lang
write_file(item, lang)
langs_written << lang
end
end
end
if langs_written.empty?
raise Twine::Error.new("Failed to genertate any files: No languages found at #{path}")
end
return 'strings.xml'
end
def determine_language_given_path(path)
@ -156,3 +135,5 @@ module Twine
end
end
end
Twine::Formatters.formatters << Twine::Formatters::Tizen.new

View file

@ -140,7 +140,7 @@ module Twine
formatter = formatter_for_format(@options[:format])
@strings.language_codes.each do |lang|
if @options[:languages] == nil || @options[:languages].length == 0 || @options[:languages].include?(lang)
file_name = lang + formatter.class::EXTENSION
file_name = lang + formatter.extension
real_path = File.join(dir, file_name)
zip_path = File.join('Locales', file_name)
formatter.write_file(real_path, lang)
@ -228,18 +228,21 @@ module Twine
end
def determine_format_given_path(path)
formatter = Formatters.formatters.find { |f| f::EXTENSION == File.extname(path) }
return formatter::FORMAT_NAME if formatter
formatter = Formatters.formatters.find { |f| f.extension == File.extname(path) }
return formatter.format_name if formatter
end
def determine_format_given_directory(directory)
formatter = Formatters.formatters.find { |f| f.can_handle_directory?(directory) }
return formatter::FORMAT_NAME if formatter
return formatter.format_name if formatter
end
def formatter_for_format(format)
formatter = Formatters.formatters.find { |f| f::FORMAT_NAME == format }
return formatter.new(@strings, @options) if formatter
formatter = Formatters.formatters.find { |f| f.format_name == format }
return nil unless formatter
formatter.strings = @strings
formatter.options = @options
formatter
end
private

View file

@ -5,8 +5,10 @@ class CommandTestCase < TwineTestCase
strings = Twine::StringsFile.new
strings.language_codes.concat KNOWN_LANGUAGES
formatter = formatter_class.new(strings, {})
formatter_class.stubs(:new).returns(formatter)
formatter = formatter_class.new
formatter.strings = strings
Twine::Formatters.formatters.clear
Twine::Formatters.formatters << formatter
formatter
end
end

View file

@ -12,7 +12,8 @@ class TestAbstractFormatter < TwineTestCase
end
end
@formatter = Twine::Formatters::Abstract.new(@strings, {})
@formatter = Twine::Formatters::Abstract.new
@formatter.strings = @strings
end
def test_set_translation_updates_existing_value
@ -46,7 +47,9 @@ class TestAbstractFormatter < TwineTestCase
end
def test_set_translation_consume_all_adds_new_key
formatter = Twine::Formatters::Abstract.new(@strings, { consume_all: true })
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.options = { consume_all: true }
formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
assert_equal 'new-key-english', @strings.strings_map['new-key'].translations['en']
@ -54,14 +57,18 @@ class TestAbstractFormatter < TwineTestCase
def test_set_translation_consume_all_adds_tags
random_tag = SecureRandom.uuid
formatter = Twine::Formatters::Abstract.new(@strings, { consume_all: true, tags: [random_tag] })
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.options = { consume_all: true, tags: [random_tag] }
formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
assert_equal [random_tag], @strings.strings_map['new-key'].tags
end
def test_set_translation_adds_new_keys_to_category_uncategoriezed
formatter = Twine::Formatters::Abstract.new(@strings, { consume_all: true })
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.options = { consume_all: true }
formatter.set_translation_for_key 'new-key', 'en', 'new-key-english'
assert_equal 'Uncategorized', @strings.sections[0].name
@ -80,7 +87,8 @@ class TestAbstractFormatter < TwineTestCase
end
end
@formatter = Twine::Formatters::Abstract.new(@strings, {})
@formatter = Twine::Formatters::Abstract.new
@formatter.strings = @strings
end
def test_set_translation_does_not_add_unchanged_translation
@ -108,14 +116,17 @@ class TestAbstractFormatter < TwineTestCase
end
def test_set_comment_for_key_does_not_update_comment
formatter = Twine::Formatters::Abstract.new(@strings, {})
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.set_comment_for_key('key', 'comment')
assert_nil formatter.strings.strings_map['key'].comment
end
def test_set_comment_for_key_updates_comment_with_update_comments
formatter = Twine::Formatters::Abstract.new(@strings, { consume_comments: true })
formatter = Twine::Formatters::Abstract.new
formatter.strings = @strings
formatter.options = { consume_comments: true }
formatter.set_comment_for_key('key', 'comment')
assert_equal 'comment', formatter.strings.strings_map['key'].comment
@ -133,7 +144,9 @@ class TestAbstractFormatter < TwineTestCase
end
end
@formatter = Twine::Formatters::Abstract.new(@strings, { consume_comments: true })
@formatter = Twine::Formatters::Abstract.new
@formatter.strings = @strings
@formatter.options = { consume_comments: true }
end
def test_set_comment_does_not_add_unchanged_comment

View file

@ -246,13 +246,13 @@ class CLITestCase < TwineTestCase
end
def test_format
random_format = Twine::Formatters.formatters.sample::FORMAT_NAME
random_format = Twine::Formatters.formatters.sample.format_name
parse_with "--format #{random_format}"
assert_equal random_format, @options[:format]
end
def test_format_ignores_case
random_format = Twine::Formatters.formatters.sample::FORMAT_NAME
random_format = Twine::Formatters.formatters.sample.format_name
parse_with "--format #{random_format.upcase}"
assert_equal random_format, @options[:format]
end

View file

@ -17,7 +17,9 @@ class FormatterTest < TwineTestCase
end
@strings = Twine::StringsFile.new
@formatter = formatter_class.new @strings, { consume_all: true, consume_comments: true }
@formatter = formatter_class.new
@formatter.strings = @strings
@formatter.options = { consume_all: true, consume_comments: true }
end
def assert_translations_read_correctly
@ -66,7 +68,8 @@ class TestAndroidFormatter < FormatterTest
end
def test_write_file_output_format
formatter = Twine::Formatters::Android.new @twine_file, {}
formatter = Twine::Formatters::Android.new
formatter.strings = @twine_file
formatter.write_file @output_path, 'en'
assert_equal content('formatter_android.xml'), output_content
end
@ -111,7 +114,8 @@ class TestAppleFormatter < FormatterTest
end
def test_write_file_output_format
formatter = Twine::Formatters::Apple.new @twine_file, {}
formatter = Twine::Formatters::Apple.new
formatter.strings = @twine_file
formatter.write_file @output_path, 'en'
assert_equal content('formatter_apple.strings'), output_content
end
@ -142,7 +146,8 @@ class TestJQueryFormatter < FormatterTest
end
def test_write_file_output_format
formatter = Twine::Formatters::JQuery.new @twine_file, {}
formatter = Twine::Formatters::JQuery.new
formatter.strings = @twine_file
formatter.write_file @output_path, 'en'
assert_equal content('formatter_jquery.json'), output_content
end
@ -171,7 +176,8 @@ class TestGettextFormatter < FormatterTest
end
def test_write_file_output_format
formatter = Twine::Formatters::Gettext.new @twine_file, {}
formatter = Twine::Formatters::Gettext.new
formatter.strings = @twine_file
formatter.write_file @output_path, 'en'
assert_equal content('formatter_gettext.po'), output_content
end
@ -192,7 +198,8 @@ class TestTizenFormatter < FormatterTest
end
def test_write_file_output_format
formatter = Twine::Formatters::Tizen.new @twine_file, {}
formatter = Twine::Formatters::Tizen.new
formatter.strings = @twine_file
formatter.write_file @output_path, 'en'
assert_equal content('formatter_tizen.xml'), output_content
end
@ -211,7 +218,8 @@ class TestDjangoFormatter < FormatterTest
end
def test_write_file_output_format
formatter = Twine::Formatters::Django.new @twine_file, {}
formatter = Twine::Formatters::Django.new
formatter.strings = @twine_file
formatter.write_file @output_path, 'en'
assert_equal content('formatter_django.po'), output_content
end
@ -229,7 +237,8 @@ class TestFlashFormatter < FormatterTest
end
def test_write_file_output_format
formatter = Twine::Formatters::Flash.new @twine_file, {}
formatter = Twine::Formatters::Flash.new
formatter.strings = @twine_file
formatter.write_file @output_path, 'en'
assert_equal content('formatter_flash.properties'), output_content
end

View file

@ -15,12 +15,17 @@ class TwineTestCase < Minitest::Test
super
Twine::stdout = StringIO.new
Twine::stderr = StringIO.new
@formatters = Twine::Formatters.formatters.dup
@output_dir = Dir.mktmpdir
@output_path = File.join @output_dir, SecureRandom.uuid
end
def teardown
FileUtils.remove_entry_secure @output_dir if File.exists? @output_dir
Twine::Formatters.formatters.clear
Twine::Formatters.formatters.concat @formatters
super
end