diff --git a/README.md b/README.md index 04d3653..495fd32 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ If you would like to enable twine to create language files in another format, cr #### `generate-string-file` -This command creates an Apple or Android strings file from the master strings data file. +This command creates an Apple or Android strings file from the master strings data file. If the output file would not contain any translations, twine will exit with an error. $ twine generate-string-file /path/to/strings.txt values-ja.xml --tags common,app1 $ twine generate-string-file /path/to/strings.txt Localizable.strings --lang ja --tags mytag @@ -108,7 +108,7 @@ This command creates an Apple or Android strings file from the master strings da #### `generate-all-string-files` -This command is a convenient way to call `generate-string-file` multiple times. It uses standard Mac OS X, iOS, and Android conventions to figure out exactly which files to create given a parent directory. For example, if you point it to a parent directory containing `en.lproj`, `fr.lproj`, and `ja.lproj` subdirectories, Twine will create a `Localizable.strings` file of the appropriate language in each of them. This is often the command you will want to execute during the build phase of your project. +This command is a convenient way to call `generate-string-file` multiple times. It uses standard Mac OS X, iOS, and Android conventions to figure out exactly which files to create given a parent directory. For example, if you point it to a parent directory containing `en.lproj`, `fr.lproj`, and `ja.lproj` subdirectories, Twine will create a `Localizable.strings` file of the appropriate language in each of them. However, files that would not contain any translations will not be created; instead warnings will be logged to `stderr`. This is often the command you will want to execute during the build phase of your project. $ twine generate-all-string-files /path/to/strings.txt /path/to/project/locales/directory --tags common,app1 @@ -128,7 +128,7 @@ This command reads in a folder containing many `.strings` or `.xml` files. These #### `generate-loc-drop` -This command is a convenient way to generate a zip file containing files created by the `generate-string-file` command. It is often used for creating a single zip containing a large number of strings in all languages which you can then hand off to your translation team. +This command is a convenient way to generate a zip file containing files created by the `generate-string-file` command. If a file would not contain any translated strings, it is skipped and a warning is logged to `stderr`. This command can be used to create a single zip containing a large number of strings in all languages which you can then hand off to your translation team. $ twine generate-loc-drop /path/to/strings.txt LocDrop1.zip $ twine generate-loc-drop /path/to/strings.txt LocDrop2.zip --lang en,fr,ja,ko --tags common,app1 diff --git a/lib/twine/formatters/abstract.rb b/lib/twine/formatters/abstract.rb index efe4e14..68d227e 100644 --- a/lib/twine/formatters/abstract.rb +++ b/lib/twine/formatters/abstract.rb @@ -87,11 +87,16 @@ module Twine raise NotImplementedError.new("You must implement read_file in your formatter class.") end - def format_file(strings, lang) + def format_file(lang) + output_processor = Processors::OutputProcessor.new(@strings, @options) + processed_strings = output_processor.process(lang) + + return nil if processed_strings.strings_map.empty? + header = format_header(lang) result = "" result += header + "\n" if header - result += format_sections(strings, lang) + result += format_sections(processed_strings, lang) end def format_header(lang) @@ -153,50 +158,6 @@ module Twine def escape_quotes(text) text.gsub('"', '\\\\"') 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' - File.open(path, "w:#{encoding}") do |f| - f.puts format_file(processed_strings, lang) - end - end - - def write_all_files(path) - file_name = @options[:file_name] || default_file_name - if @options[:create_folders] - @strings.language_codes.each do |lang| - output_path = File.join(path, output_path_for_language(lang)) - - FileUtils.mkdir_p(output_path) - - file_path = File.join(output_path, file_name) - write_file(file_path, lang) - end - else - language_written = false - Dir.foreach(path) do |item| - next if item == "." or item == ".." - - item = File.join(path, item) - next unless File.directory?(item) - - lang = determine_language_given_path(item) - next unless lang - - file_path = File.join(item, file_name) - write_file(file_path, lang) - language_written = true - end - - if !language_written - raise Twine::Error.new("Failed to generate any files: No languages found at #{path}") - end - end - end - end end end diff --git a/lib/twine/formatters/android.rb b/lib/twine/formatters/android.rb index b7c8401..96de694 100644 --- a/lib/twine/formatters/android.rb +++ b/lib/twine/formatters/android.rb @@ -108,7 +108,7 @@ module Twine result += super + "\n" - result += '' + result += "\n" end def format_section_header(section) diff --git a/lib/twine/formatters/django.rb b/lib/twine/formatters/django.rb index 2417eaf..d0b5083 100644 --- a/lib/twine/formatters/django.rb +++ b/lib/twine/formatters/django.rb @@ -94,8 +94,8 @@ module Twine end end - def format_file(strings, lang) - @default_lang = strings.language_codes[0] + def format_file(lang) + @default_lang = @strings.language_codes[0] result = super @default_lang = nil result diff --git a/lib/twine/formatters/flash.rb b/lib/twine/formatters/flash.rb index db8ccf1..c0dbada 100644 --- a/lib/twine/formatters/flash.rb +++ b/lib/twine/formatters/flash.rb @@ -74,6 +74,10 @@ module Twine end end + def format_sections(strings, lang) + super + "\n" + end + def format_header(lang) "## Flash Strings File\n## Generated by Twine #{Twine::VERSION}\n## Language: #{lang}" end diff --git a/lib/twine/formatters/gettext.rb b/lib/twine/formatters/gettext.rb index e08d934..d49bc4a 100644 --- a/lib/twine/formatters/gettext.rb +++ b/lib/twine/formatters/gettext.rb @@ -64,7 +64,7 @@ module Twine end end - def format_file(strings, lang) + def format_file(lang) @default_lang = strings.language_codes[0] result = super @default_lang = nil diff --git a/lib/twine/formatters/jquery.rb b/lib/twine/formatters/jquery.rb index a6968c2..badab5a 100644 --- a/lib/twine/formatters/jquery.rb +++ b/lib/twine/formatters/jquery.rb @@ -44,8 +44,10 @@ module Twine end end - def format_file(strings, lang) - "{\n#{super}\n}" + def format_file(lang) + result = super + return result unless result + "{\n#{super}\n}\n" end def format_sections(strings, lang) diff --git a/lib/twine/formatters/tizen.rb b/lib/twine/formatters/tizen.rb index 5ec6716..c4abd99 100644 --- a/lib/twine/formatters/tizen.rb +++ b/lib/twine/formatters/tizen.rb @@ -101,7 +101,7 @@ module Twine result += super + "\n" - result += '' + result += "\n" end def format_section_header(section) diff --git a/lib/twine/runner.rb b/lib/twine/runner.rb index f113a11..f91d0da 100644 --- a/lib/twine/runner.rb +++ b/lib/twine/runner.rb @@ -48,7 +48,12 @@ module Twine lang = nil lang = @options[:languages][0] if @options[:languages] - write_string_file(@options[:output_path], lang) + formatter, lang = prepare_read_write(@options[:output_path], lang) + output = formatter.format_file(lang) + + raise Twine::Error.new "Nothing to generate! The resulting file would not contain any strings." unless output + + IO.write(@options[:output_path], output, encoding: encoding) end def generate_all_string_files @@ -69,7 +74,51 @@ module Twine raise Twine::Error.new "Could not determine format given the contents of #{@options[:output_path]}" end - formatter.write_all_files(@options[:output_path]) + file_name = @options[:file_name] || formatter.default_file_name + if @options[:create_folders] + @strings.language_codes.each do |lang| + output_path = File.join(@options[:output_path], formatter.output_path_for_language(lang)) + + FileUtils.mkdir_p(output_path) + + file_path = File.join(output_path, file_name) + + output = formatter.format_file(lang) + unless output + Twine::stderr.puts "Skipping file at path #{file_path} since it would not contain any strings." + next + end + + IO.write(file_path, output, encoding: encoding) + end + else + language_found = false + Dir.foreach(@options[:output_path]) do |item| + next if item == "." or item == ".." + + output_path = File.join(@options[:output_path], item) + next unless File.directory?(output_path) + + lang = formatter.determine_language_given_path(output_path) + next unless lang + + language_found = true + + file_path = File.join(output_path, file_name) + output = formatter.format_file(lang) + unless output + Twine::stderr.puts "Skipping file at path #{file_path} since it would not contain any strings." + next + end + + IO.write(file_path, output, encoding: encoding) + end + + unless language_found + raise Twine::Error.new("Failed to generate any files: No languages found at #{@options[:output_path]}") + end + end + end def consume_string_file @@ -111,7 +160,7 @@ module Twine File.delete(@options[:output_path]) end - Dir.mktmpdir do |dir| + Dir.mktmpdir do |temp_dir| Zip::File.open(@options[:output_path], Zip::File::CREATE) do |zipfile| zipfile.mkdir('Locales') @@ -119,10 +168,17 @@ module Twine @strings.language_codes.each do |lang| if @options[:languages] == nil || @options[:languages].length == 0 || @options[:languages].include?(lang) file_name = lang + formatter.extension - real_path = File.join(dir, file_name) + temp_path = File.join(temp_dir, file_name) zip_path = File.join('Locales', file_name) - formatter.write_file(real_path, lang) - zipfile.add(zip_path, real_path) + + output = formatter.format_file(lang) + unless output + Twine::stderr.puts "Skipping file #{file_name} since it would not contain any strings." + next + end + + IO.write(temp_path, output, encoding: encoding) + zipfile.add(zip_path, temp_path) end end end @@ -204,6 +260,10 @@ module Twine private + def encoding + @options[:output_encoding] || 'UTF-8' + end + def require_rubyzip begin require 'zip' @@ -235,15 +295,9 @@ module Twine end formatter, lang = prepare_read_write(path, lang) - formatter.read_file(path, lang) end - def write_string_file(path, lang) - formatter, lang = prepare_read_write(path, lang) - formatter.write_file(path, lang) - end - def prepare_read_write(path, lang) formatter_for_path = find_formatter { |f| f.extension == File.extname(path) } formatter = formatter_for_format(@options[:format]) || formatter_for_path diff --git a/test/test_formatters.rb b/test/test_formatters.rb index 09d19cf..b9ebe28 100644 --- a/test/test_formatters.rb +++ b/test/test_formatters.rb @@ -67,11 +67,10 @@ class TestAndroidFormatter < FormatterTest assert_equal '@value', @strings.strings_map['key1'].translations['en'] end - def test_write_file_output_format + def test_format_file formatter = Twine::Formatters::Android.new formatter.strings = @twine_file - formatter.write_file @output_path, 'en' - assert_equal content('formatter_android.xml'), output_content + assert_equal content('formatter_android.xml'), formatter.format_file('en') end def test_format_key_with_space @@ -115,7 +114,7 @@ class TestAndroidFormatter < FormatterTest end def test_does_not_deduct_language_from_device_capability_resource_folder - assert_nil @formatter.determine_language_given_path('res/values-w820p') + assert_nil @formatter.determine_language_given_path('res/values-w820dp') end end @@ -130,11 +129,10 @@ class TestAppleFormatter < FormatterTest assert_file_contents_read_correctly end - def test_write_file_output_format + def test_format_file formatter = Twine::Formatters::Apple.new formatter.strings = @twine_file - formatter.write_file @output_path, 'en' - assert_equal content('formatter_apple.strings'), output_content + assert_equal content('formatter_apple.strings'), formatter.format_file('en') end def test_format_key_with_space @@ -162,11 +160,10 @@ class TestJQueryFormatter < FormatterTest assert_translations_read_correctly end - def test_write_file_output_format + def test_format_file formatter = Twine::Formatters::JQuery.new formatter.strings = @twine_file - formatter.write_file @output_path, 'en' - assert_equal content('formatter_jquery.json'), output_content + assert_equal content('formatter_jquery.json'), formatter.format_file('en') end def test_format_value_with_newline @@ -192,11 +189,10 @@ class TestGettextFormatter < FormatterTest assert_equal 'multiline\nstring', @strings.strings_map['key1'].translations['en'] end - def test_write_file_output_format + def test_format_file formatter = Twine::Formatters::Gettext.new formatter.strings = @twine_file - formatter.write_file @output_path, 'en' - assert_equal content('formatter_gettext.po'), output_content + assert_equal content('formatter_gettext.po'), formatter.format_file('en') end end @@ -214,11 +210,10 @@ class TestTizenFormatter < FormatterTest assert_file_contents_read_correctly end - def test_write_file_output_format + def test_format_file formatter = Twine::Formatters::Tizen.new formatter.strings = @twine_file - formatter.write_file @output_path, 'en' - assert_equal content('formatter_tizen.xml'), output_content + assert_equal content('formatter_tizen.xml'), formatter.format_file('en') end end @@ -234,11 +229,10 @@ class TestDjangoFormatter < FormatterTest assert_file_contents_read_correctly end - def test_write_file_output_format + def test_format_file formatter = Twine::Formatters::Django.new formatter.strings = @twine_file - formatter.write_file @output_path, 'en' - assert_equal content('formatter_django.po'), output_content + assert_equal content('formatter_django.po'), formatter.format_file('en') end end @@ -253,10 +247,9 @@ class TestFlashFormatter < FormatterTest assert_file_contents_read_correctly end - def test_write_file_output_format + def test_format_file formatter = Twine::Formatters::Flash.new formatter.strings = @twine_file - formatter.write_file @output_path, 'en' - assert_equal content('formatter_flash.properties'), output_content + assert_equal content('formatter_flash.properties'), formatter.format_file('en') end end diff --git a/test/test_generate_all_string_files.rb b/test/test_generate_all_string_files.rb index 4a9b8f3..97c7ba8 100644 --- a/test/test_generate_all_string_files.rb +++ b/test/test_generate_all_string_files.rb @@ -1,48 +1,74 @@ require 'command_test_case' class TestGenerateAllStringFiles < CommandTestCase - class TestCreateFolders < CommandTestCase - def new_runner(create_folders) - options = {} - options[:output_path] = @output_dir - options[:format] = 'apple' - options[:create_folders] = create_folders + def new_runner(create_folders, twine_file = nil) + options = {} + options[:output_path] = @output_dir + options[:format] = 'apple' + options[:create_folders] = create_folders + unless twine_file twine_file = build_twine_file 'en', 'es' do add_section 'Section' do add_row key: 'value' end end + end - Twine::Runner.new(options, twine_file) + Twine::Runner.new(options, twine_file) + end + + class TestDoNotCreateFolders < TestGenerateAllStringFiles + def new_runner(twine_file = nil) + super(false, twine_file) end def test_fails_if_output_folder_does_not_exist assert_raises Twine::Error do - new_runner(false).generate_all_string_files + new_runner.generate_all_string_files end end + def test_does_not_create_language_folders + Dir.mkdir File.join @output_dir, 'en.lproj' + new_runner.generate_all_string_files + refute File.exists?(File.join(@output_dir, 'es.lproj')), "language folder should not be created" + end + + def test_prints_empty_file_warnings + Dir.mkdir File.join @output_dir, 'en.lproj' + empty_twine_file = build_twine_file('en') {} + new_runner(empty_twine_file).generate_all_string_files + assert_match "Skipping file at path", Twine::stderr.string + end + end + + class TestCreateFolders < TestGenerateAllStringFiles + def new_runner(twine_file = nil) + super(true, twine_file) + end + def test_creates_output_folder FileUtils.remove_entry_secure @output_dir - new_runner(true).generate_all_string_files + new_runner.generate_all_string_files assert File.exists? @output_dir end - def test_does_not_create_language_folders_by_default - Dir.mkdir File.join @output_dir, 'en.lproj' - new_runner(false).generate_all_string_files - refute File.exists?(File.join(@output_dir, 'es.lproj')), "language folder should not be created" - end - def test_creates_language_folders - new_runner(true).generate_all_string_files + new_runner.generate_all_string_files assert File.exists?(File.join(@output_dir, 'en.lproj')), "language folder 'en.lproj' should be created" assert File.exists?(File.join(@output_dir, 'es.lproj')), "language folder 'es.lproj' should be created" end + + def test_prints_empty_file_warnings + empty_twine_file = build_twine_file('en') {} + new_runner(empty_twine_file).generate_all_string_files + + assert_match "Skipping file at path", Twine::stderr.string + end end - class TestDeliberate < CommandTestCase + class TestValidate < CommandTestCase def new_runner(validate) Dir.mkdir File.join @output_dir, 'values-en' diff --git a/test/test_generate_loc_drop.rb b/test/test_generate_loc_drop.rb index 4ef0979..4912198 100644 --- a/test/test_generate_loc_drop.rb +++ b/test/test_generate_loc_drop.rb @@ -1,30 +1,30 @@ require 'command_test_case' class TestGenerateLocDrop < CommandTestCase - def setup - super - + def new_runner(twine_file = nil) options = {} options[:output_path] = @output_path options[:format] = 'apple' - @twine_file = build_twine_file 'en', 'fr' do - add_section 'Section' do - add_row key: 'value' + unless twine_file + twine_file = build_twine_file 'en', 'fr' do + add_section 'Section' do + add_row key: 'value' + end end end - @runner = Twine::Runner.new(options, @twine_file) + Twine::Runner.new(options, twine_file) end def test_generates_zip_file - @runner.generate_loc_drop + new_runner.generate_loc_drop - assert File.exists?(@output_path), "language folder should not be created" + assert File.exists?(@output_path), "zip file should exist" end def test_zip_file_structure - @runner.generate_loc_drop + new_runner.generate_loc_drop names = [] Zip::File.open(@output_path) do |zipfile| @@ -37,12 +37,18 @@ class TestGenerateLocDrop < CommandTestCase def test_uses_formatter formatter = prepare_mock_formatter Twine::Formatters::Apple - formatter.expects(:write_file).twice.with() { |path, lang| FileUtils.touch path } + formatter.expects(:format_file).twice - @runner.generate_loc_drop + new_runner.generate_loc_drop end - class TestDeliberate < CommandTestCase + def test_prints_empty_file_warnings + empty_twine_file = build_twine_file('en') {} + new_runner(empty_twine_file).generate_loc_drop + assert_match "Skipping file", Twine::stderr.string + end + + class TestValidate < CommandTestCase def new_runner(validate) options = {} options[:output_path] = @output_path diff --git a/test/test_generate_string_file.rb b/test/test_generate_string_file.rb index aa27eec..d6d3835 100644 --- a/test/test_generate_string_file.rb +++ b/test/test_generate_string_file.rb @@ -12,31 +12,31 @@ class TestGenerateStringFile < CommandTestCase Twine::Runner.new(options, strings) end - def prepare_mock_write_file_formatter(formatter_class) + def prepare_mock_format_file_formatter(formatter_class) formatter = prepare_mock_formatter(formatter_class) - formatter.expects(:write_file) + formatter.expects(:format_file).returns(true) end def test_deducts_android_format_from_output_path - prepare_mock_write_file_formatter Twine::Formatters::Android + prepare_mock_format_file_formatter Twine::Formatters::Android new_runner('fr', 'fr.xml').generate_string_file end def test_deducts_apple_format_from_output_path - prepare_mock_write_file_formatter Twine::Formatters::Apple + prepare_mock_format_file_formatter Twine::Formatters::Apple new_runner('fr', 'fr.strings').generate_string_file end def test_deducts_jquery_format_from_output_path - prepare_mock_write_file_formatter Twine::Formatters::JQuery + prepare_mock_format_file_formatter Twine::Formatters::JQuery new_runner('fr', 'fr.json').generate_string_file end def test_deducts_gettext_format_from_output_path - prepare_mock_write_file_formatter Twine::Formatters::Gettext + prepare_mock_format_file_formatter Twine::Formatters::Gettext new_runner('fr', 'fr.po').generate_string_file end @@ -44,12 +44,21 @@ class TestGenerateStringFile < CommandTestCase def test_deducts_language_from_output_path random_language = KNOWN_LANGUAGES.sample formatter = prepare_mock_formatter Twine::Formatters::Android - formatter.expects(:write_file).with(anything, random_language) + formatter.expects(:format_file).with(random_language).returns(true) new_runner(nil, "#{random_language}.xml").generate_string_file end - class TestDeliberate < CommandTestCase + def test_returns_error_if_nothing_written + formatter = prepare_mock_formatter Twine::Formatters::Android + formatter.expects(:format_file).returns(false) + + assert_raises Twine::Error do + new_runner('fr', 'fr.xml').generate_string_file + end + end + + class TestValidate < CommandTestCase def new_runner(validate) options = {} options[:output_path] = @output_path diff --git a/test/test_strings_file.rb b/test/test_strings_file.rb index ee27140..a3b9788 100644 --- a/test/test_strings_file.rb +++ b/test/test_strings_file.rb @@ -50,7 +50,7 @@ class TestStringsFile < TwineTestCase @strings.write @output_path - assert_equal content('twine_accent_values.txt'), output_content + assert_equal content('twine_accent_values.txt'), File.read(@output_path) end end diff --git a/test/twine_test_case.rb b/test/twine_test_case.rb index 766d070..f9753b3 100644 --- a/test/twine_test_case.rb +++ b/test/twine_test_case.rb @@ -29,10 +29,6 @@ class TwineTestCase < Minitest::Test super end - def output_content - File.read @output_path - end - def execute(command) command += " -o #{@output_path}" Twine::Runner.run(command.split(" "))