diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index fa275a45..0ad41251 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -18,8 +18,6 @@ Lint/SelfAssignment: RSpec/ContextWording: Exclude: - 'spec/commands/tree_commands_spec.rb' - - 'spec/deepl_translate_spec.rb' - - 'spec/google_translate_spec.rb' - 'spec/locale_tree/siblings_spec.rb' - 'spec/relative_keys_spec.rb' @@ -39,7 +37,6 @@ RSpec/ExampleLength: # DisallowedExamples: works RSpec/ExampleWording: Exclude: - - 'spec/deepl_translate_spec.rb' - 'spec/google_translate_spec.rb' - 'spec/relative_keys_spec.rb' diff --git a/CHANGES.md b/CHANGES.md index 1a0601d5..816aca82 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -24,6 +24,7 @@ to use the Prism Scanner without Rails support. - Loads environment variables via `dotenv` if available. [#395](https://github.com/glebm/i18n-tasks/issues/395) - Adds CLI command `check_prism` to try the new parser out and see the differences in key detection. - The Prism-based scanner supports candidate_keys for Rails translations, allowing relative translations in controllers to match either the key scoped to controller and action or only to the controller. +- Translation services now catch errors and save partial results [#642](https://github.com/glebm/i18n-tasks/issues/642) ## v1.0.15 diff --git a/lib/i18n/tasks/translators/base_translator.rb b/lib/i18n/tasks/translators/base_translator.rb index cfe9d95f..4fadc7d8 100644 --- a/lib/i18n/tasks/translators/base_translator.rb +++ b/lib/i18n/tasks/translators/base_translator.rb @@ -21,7 +21,22 @@ def translate_forest(forest, from) @progress_bar = ProgressBar.create(total: pairs.flatten.size, format: "%a <%B> %e %c/%C (%p%%)") - translated = translate_pairs(pairs, to: root.key, from: from) + begin + translated = translate_pairs(pairs, to: root.key, from: from) + rescue => e + warn "Translation for locale #{root.key} failed: #{e.message}" + # If translate_pairs raised, try to salvage any partial translations + # by attempting to translate each slice individually and collecting successes. + translated = [] + pairs.group_by { |k_v| @i18n_tasks.html_key? k_v[0], from }.each do |_is_html, list_slice| + translated.concat(fetch_translations(list_slice, to: root.key, from: from)) + rescue => e2 + warn "Partial translation failed for locale #{root.key}: #{e2.message} - leaving keys untranslated" + # leave the original list_slice untranslated + translated.concat(list_slice) + end + end + result.merge! Data::Tree::Siblings.from_flat_pairs(translated) end end @@ -40,6 +55,10 @@ def translate_pairs(list, opts) list -= reference_key_vals result = list.group_by { |k_v| @i18n_tasks.html_key? k_v[0], opts[:from] }.map do |is_html, list_slice| fetch_translations(list_slice, opts.merge(is_html ? options_for_html : options_for_plain)) + rescue => e + warn "Translation slice failed: #{e.message} - leaving slice untranslated" + # Return the original untranslated slice so already completed translations are preserved + list_slice end.reduce(:+) || [] result.concat(reference_key_vals) result.sort! { |a, b| key_pos[a[0]] <=> key_pos[b[0]] } @@ -128,10 +147,10 @@ def replace_interpolations(value) # @param [String] translated # @return [String] 'hello, ' => 'hello, %{name}' def restore_interpolations(untranslated, translated) - return translated if untranslated !~ INTERPOLATION_KEY_RE + return translated if !INTERPOLATION_KEY_RE.match?(untranslated) values = untranslated.scan(INTERPOLATION_KEY_RE) - translated.gsub(/#{Regexp.escape(UNTRANSLATABLE_STRING)}\d+/i) do |m| + translated.gsub(/#{Regexp.escape(UNTRANSLATABLE_STRING)}\d+/io) do |m| values[m[UNTRANSLATABLE_STRING.length..].to_i] end rescue => e diff --git a/spec/translators/base_translator_spec.rb b/spec/translators/base_translator_spec.rb new file mode 100644 index 00000000..1c68ce2f --- /dev/null +++ b/spec/translators/base_translator_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe "Base Translator" do + let(:task) { I18n::Tasks::BaseTask.new } + + # Create a fake translator that raises for html slices + let(:translator_class) do + Class.new(I18n::Tasks::Translators::BaseTranslator) do + def translate_values(list, **options) + if options[:html] + raise StandardError, "html translation failure" + end + + # return translated values simply by appending `-es` for testing + list.map { |v| "#{v}-es" } + end + + def options_for_translate_values(from:, to:, **options) + options.merge(from: from, to: to) + end + + def options_for_html + {html: true} + end + + def options_for_plain + {html: false} + end + + def no_results_error_message + "no results" + end + end + end + + it "preserves successful translations when a subsequent slice fails" do + translator = translator_class.new(task) + + list = [ + ["common.plain", "Hello"], + ["common.html.html", "Hi"] + ] + + result = translator.send(:translate_pairs, list, from: "en", to: "es") + + # Find translated plain key + plain = result.assoc("common.plain") + expect(plain).not_to be_nil + expect(plain.last).to eq("Hello-es") + + # HTML slice should have been left untranslated due to simulated failure + html = result.assoc("common.html.html") + expect(html).not_to be_nil + expect(html.last).to eq("Hi") + end +end diff --git a/spec/deepl_translate_spec.rb b/spec/translators/deepl_translate_spec.rb similarity index 98% rename from spec/deepl_translate_spec.rb rename to spec/translators/deepl_translate_spec.rb index fa9a380a..3c3db11a 100644 --- a/spec/deepl_translate_spec.rb +++ b/spec/translators/deepl_translate_spec.rb @@ -31,10 +31,10 @@ TestCodebase.teardown end - context "command" do + context "with default" do let(:task) { i18n_task } - it "works" do + it "translate-missing" do skip "temporarily disabled on JRuby due to https://github.com/jruby/jruby/issues/4802" if RUBY_ENGINE == "jruby" skip "DEEPL_AUTH_KEY env var not set" unless ENV["DEEPL_AUTH_KEY"] in_test_app_dir do diff --git a/spec/google_translate_spec.rb b/spec/translators/google_translate_spec.rb similarity index 98% rename from spec/google_translate_spec.rb rename to spec/translators/google_translate_spec.rb index 272fcd31..6eebfa5e 100644 --- a/spec/google_translate_spec.rb +++ b/spec/translators/google_translate_spec.rb @@ -42,10 +42,10 @@ TestCodebase.teardown end - context "command" do + context "with default" do let(:task) { i18n_task } - it "works" do + it "translate-missing" do skip "GOOGLE_TRANSLATE_API_KEY env var not set" unless ENV["GOOGLE_TRANSLATE_API_KEY"] skip "GOOGLE_TRANSLATE_API_KEY env var is empty" if ENV["GOOGLE_TRANSLATE_API_KEY"].empty? in_test_app_dir do @@ -113,7 +113,7 @@ TestCodebase.teardown end - context "command" do + context "when translate-missing" do let(:task) { i18n_task } it "allows google to decide proper language from locale" do