diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..cf5b4ad59 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# 2 space indentation +[*.rb] +indent_style = space +indent_size = 2 \ No newline at end of file diff --git a/package/yast2-packager.changes b/package/yast2-packager.changes index 088d591e2..0c1370fe0 100644 --- a/package/yast2-packager.changes +++ b/package/yast2-packager.changes @@ -1,3 +1,10 @@ +------------------------------------------------------------------- +Thu Jun 13 17:15:37 CEST 2019 - David Diaz + +- Allow to select the license language when running in textmode + (bsc#1135901) +- 4.1.45 + ------------------------------------------------------------------- Thu May 30 12:59:43 UTC 2019 - Ladislav Slezák diff --git a/package/yast2-packager.spec b/package/yast2-packager.spec index fc914c3d4..0335df5ec 100644 --- a/package/yast2-packager.spec +++ b/package/yast2-packager.spec @@ -17,7 +17,7 @@ Name: yast2-packager -Version: 4.1.44 +Version: 4.1.45 Release: 0 BuildRoot: %{_tmppath}/%{name}-%{version}-build @@ -137,6 +137,7 @@ rake install DESTDIR="%{buildroot}" %{yast_ybindir}/* %{yast_yncludedir}/checkmedia/* %{yast_yncludedir}/packager/* +%{yast_libdir}/language_tag.rb %{yast_libdir}/packager/* %{yast_libdir}/packager/cfa/* %{yast_libdir}/y2packager/* diff --git a/src/lib/language_tag.rb b/src/lib/language_tag.rb new file mode 100644 index 000000000..1d58b3158 --- /dev/null +++ b/src/lib/language_tag.rb @@ -0,0 +1,99 @@ +# ------------------------------------------------------------------------------ +# Copyright (c) 2019 SUSE LLC, All Rights Reserved. +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of version 2 of the GNU General Public License as published by the +# Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# ------------------------------------------------------------------------------ + +require "yast" + +# {::Comparable} enforces a total ordering, contrary to its +# documentation, WTF. +module PartiallyComparable + def <(other) + cmp = self.<=>(other) + return nil if cmp.nil? + cmp < 0 + end + + def >(other) + cmp = self.<=>(other) + return nil if cmp.nil? + cmp > 0 + end + + def <=(other) + cmp = self.<=>(other) + return nil if cmp.nil? + cmp <= 0 + end + + def >=(other) + cmp = self.<=>(other) + return nil if cmp.nil? + cmp >= 0 + end + + def ==(other) + return true if equal?(other) # object identity + cmp = self.<=>(other) + return nil if cmp.nil? + cmp == 0 + end +end + +# Language tags like "cs" "cs_CZ" "cs_CZ.UTF-8". +# +# FIXME: improve the simplistic string comparisons +class LanguageTag + include Yast::Logger + + # @param s [String] + def initialize(s) + @tag = s + end + + def to_s + @tag + end + + include PartiallyComparable + + # Like with classes (where Special < General) "en_US" < "en" + # Mnemonics: number of speakers + def <=>(other) + return 0 if to_s == other.to_s + return -1 if to_s.start_with?(other.to_s) + return 1 if other.to_s.start_with?(to_s) + nil + end + + # A more general tag: "en_US" -> "en" (-> nil) + # @return [LanguageTag,nil] + def generalize + self.class.new(@tag.split("_").first) if @tag.include? "_" + # else nil + # FIXME: or self, find out what makes more sense + end + + # @return [String,nil] + def name(lang_map_cache: nil) + lang_map_cache ||= Yast::Language.GetLanguagesMap(false) + attrs = lang_map_cache[@tag] + if attrs.nil? + # we're en, find en_US + _tag, attrs = lang_map_cache.find { |k, _v| self > LanguageTag.new(k) } + end + if attrs.nil? + log.warn "Could not find name for language '#{@tag}'" + return nil + end + + attrs[4] + end +end diff --git a/src/lib/y2packager/widgets/product_license_translations.rb b/src/lib/y2packager/widgets/product_license_translations.rb index 915fb004f..cee4b9786 100644 --- a/src/lib/y2packager/widgets/product_license_translations.rb +++ b/src/lib/y2packager/widgets/product_license_translations.rb @@ -12,6 +12,7 @@ require "yast" require "cwm" +require "language_tag" require "y2packager/widgets/simple_language_selection" require "y2packager/widgets/product_license" @@ -71,7 +72,7 @@ def handle(event) # @return [Y2Packager::Widgets::SimpleLanguageSelection] def language_selection @language_selection ||= - Y2Packager::Widgets::SimpleLanguageSelection.new(available_locales, content_language) + Y2Packager::Widgets::SimpleLanguageSelection.new(selectable_locales, content_language) end # Product selection widget @@ -82,49 +83,38 @@ def product_license Y2Packager::Widgets::ProductLicenseContent.new(product, content_language) end - # Available license translations + # Selectable license translations # - # When running on textmode, only the preselected/given language is considered. + # When running on textmode, the terminal is not able to display *some* languages # see #default_language for further details. # # @return [Array] Locale codes of the available translations - # @see #default_language - def available_locales - Yast::UI.TextMode ? [default_language] : product.license_locales + def selectable_locales + product.license_locales.find_all { |loc| displayable_language?(loc) } end # License translation language # - # When running on textmode, it returns the preselected/default language. - # see #default_language for further details. + # If the wanted language is present among those displayable, use it, + # otherwise use the default # # @return [String] License content language - # @see #default_language def content_language - Yast::UI.TextMode ? default_language : language + # this selects "en" if we want "en_US" + l = selectable_locales.find { |loc| LanguageTag.new(loc) >= language } + l || DEFAULT_FALLBACK_LANGUAGE end # @return [String] Fallback language DEFAULT_FALLBACK_LANGUAGE = "en_US".freeze - # Default language - # - # For some languages (like Japanese, Chinese or Korean) YaST needs to use a fbiterm in order - # to display symbols correctly when running on textmode. However, if none of those languages - # is selected on boot, this special terminal won't be used. - # - # So during 1st stage and when running in textmode, it returns the preselected language (from - # install.inf). - # - # On an installed system, it prefers the given language. Finally, if the license translation - # is not available, the fallback language is returned. + # Whether a language is displayable # - # @return [String] Language code - def default_language - candidate_lang = Yast::Stage.initial ? Yast::Language.preselected : language - translated = product.license_locales.any? { |l| candidate_lang.start_with?(l) } - return candidate_lang if translated - DEFAULT_FALLBACK_LANGUAGE + # @param lang [String] "cs" or "cs_CZ" + # @return [Boolean] + # @see Yast::Language.supported_language? + def displayable_language?(lang) + Yast::Language.supported_language?(lang) end end end diff --git a/src/lib/y2packager/widgets/simple_language_selection.rb b/src/lib/y2packager/widgets/simple_language_selection.rb index b6ea0e741..1ef2a66f4 100644 --- a/src/lib/y2packager/widgets/simple_language_selection.rb +++ b/src/lib/y2packager/widgets/simple_language_selection.rb @@ -19,6 +19,7 @@ require "yast" require "cwm/widget" +require "language_tag" Yast.import "Language" @@ -69,14 +70,11 @@ def opt # initial value will be set to "en_US". def init languages = items.map(&:first) - new_value = - if languages.include?(default) - default - elsif default.include?("_") - short_code = default.split("_").first - languages.include?(short_code) ? short_code : nil - end - + candidates = [ + default, + LanguageTag.new(default).generalize.to_s + ] + new_value = candidates.compact.find { |c| languages.include?(c) } self.value = new_value || DEFAULT_LICENSE_LANG end @@ -92,19 +90,11 @@ def help # @return [Array>] Array of languages in form [code, description] def items return @items if @items - languages_map = Yast::Language.GetLanguagesMap(false) - @items = languages.each_with_object([]) do |code, langs| - attrs = languages_map.key?(code) ? languages_map[code] : nil - lang, attrs = languages_map.find { |k, _v| k.start_with?(code) } if attrs.nil? - - if attrs.nil? - log.warn "Not valid language '#{lang}'" - next - end - - log.debug "Using language '#{lang}' instead of '#{code}'" if lang != code - langs << [code, attrs[4]] + lmap = Yast::Language.GetLanguagesMap(false) + @items = languages.map do |lang| + [lang, LanguageTag.new(lang).name(lang_map_cache: lmap)] end + @items.reject! { |_lang, name| name.nil? } @items.uniq! @items.sort_by!(&:last) end diff --git a/src/modules/ProductLicense.rb b/src/modules/ProductLicense.rb index 7638b0c1f..680a2604f 100644 --- a/src/modules/ProductLicense.rb +++ b/src/modules/ProductLicense.rb @@ -569,6 +569,7 @@ def AskInstalledLicensesAgreement(directories, action) # FIXME: this is needed only by yast2-registration, fix it later # and make this method private + # @param licenses [ArgRef] a map $[ lang_code : filename ] def HandleLicenseDialogRet(licenses, base_product, action) ret = nil @@ -661,6 +662,12 @@ def HandleLicenseDialogRet(licenses, base_product, action) # @return [Array] Fallback languages DEFAULT_FALLBACK_LANGUAGES = ["en_US", "en"].freeze + def displayable_language?(lang) + return true if lang.empty? # zypp means English here + Yast::Language.supported_language?(lang) + end + private :displayable_language? + # FIXME: this is needed only by yast2-registration, fix it later # and make this method private # @@ -668,35 +675,19 @@ def HandleLicenseDialogRet(licenses, base_product, action) # @param [Array] languages list of license translations # @param [Boolean] back enable "Back" button # @param [String] license_language default license language - # @param [Hash] licenses licenses (mapping "language_code" => "license") + # @param licenses [ArgRef] a map $[ lang_code : filename ] # @param [String] id unique license ID # @param [String] caption dialog title def DisplayLicenseDialogWithTitle(languages, back, license_language, licenses, id, caption) - languages = deep_copy(languages) - - # For some languages (like Japanese, Chinese or Korean) YaST needs to use a fbiterm in order - # to display symbols correctly when running on textmode. To avoid such problems, consider only - # the preselected (on installation) or the default language (on running system). This will - # setup fbiterm correctly. See bsc#1094793 for further information. - if Yast::UI.TextMode - lang = default_language - candidate_languages = [lang, lang[0..1]] + DEFAULT_FALLBACK_LANGUAGES - license_language = (candidate_languages & languages).first || "" - languages = [license_language] - log.info "Adjusted license language to #{license_language}" - end + languages = languages.find_all { |lang| displayable_language?(lang) } + log.info "Displayable languages: #{languages}, wanted: #{license_language}" - contents = ( - licenses_ref = arg_ref(licenses.value) - result = GetLicenseDialog( - languages, - license_language, - licenses_ref, - id, - false - ) - licenses.value = licenses_ref.value - result + contents = GetLicenseDialog( + languages, + license_language, + licenses, + id, + false ) Wizard.SetContents( @@ -770,7 +761,7 @@ def license_download_label(display_url) # update license location displayed in the dialog (e.g. after license translation # is changed) # @param [String] lang language of the currently displayed license - # @param [Yast::ArgRef] licenses reference to the list of licenses + # @param licenses [ArgRef] a map $[ lang_code : filename ] def update_license_location(lang, licenses) return if !location_is_url?(license_file_print) || !UI.WidgetExists(:printing_hint) @@ -849,6 +840,7 @@ def repository_product(src_id) Y2Packager::Product.from_h(product_h) end + # @param licenses [ArgRef] a map $[ lang_code : filename ] def GetLicenseContent(lic_lang, licenses, id) license_file = ( licenses_ref = arg_ref(licenses.value) @@ -936,6 +928,7 @@ def LicenseHasBeenAccepted(license_ident) nil end + # @param licenses [ArgRef] a map $[ lang_code : filename ] def WhichLicenceFile(license_language, licenses) license_file = Ops.get(licenses.value, license_language, "") @@ -952,6 +945,7 @@ def WhichLicenceFile(license_language, licenses) license_file end + # @param licenses [ArgRef] a map $[ lang_code : filename ] def GetLicenseDialogTerm(languages, license_language, licenses, id) languages = deep_copy(languages) rt = ( @@ -1083,6 +1077,11 @@ def base_product_id current_sources.any? ? current_sources.first : 0 end + # @param [Array] languages list of license translations + # @param [String] license_language default license language + # @param licenses [ArgRef] a map $[ lang_code : filename ] + # @param [String] id unique license ID + # @param [Boolean] spare_space def GetLicenseDialog(languages, license_language, licenses, id, spare_space) space = UI.TextMode ? 1 : 3 @@ -1143,6 +1142,7 @@ def GetLicenseDialog(languages, license_language, licenses, id, spare_space) end # Displays License dialog + # @param licenses [ArgRef] a map $[ lang_code : filename ] def DisplayLicenseDialog(languages, back, license_language, licenses, id) # dialog title DisplayLicenseDialogWithTitle(languages, back, license_language, licenses, id, @@ -1166,7 +1166,7 @@ def CleanUpLicense(tmpdir) # @param [String] dir string directory to look into # @param [Array] patterns a list of patterns for the files, regular expressions # with %1 for the language - # @return a map $[ lang_code : filename ] + # @return [Hash{String, String}] a map $[ lang_code : filename ] def LicenseFiles(dir, patterns) patterns = deep_copy(patterns) ret = {} @@ -1494,6 +1494,8 @@ def cache_license_acceptance_needed(id, license_dir) SetAcceptanceNeeded(id, license_acceptance_needed) end + # @param licenses [ArgRef] a map $[ lang_code : filename ] + # @return [:cont,:auto] def InitLicenseData(src_id, dir, licenses, available_langs, _require_agreement, _license_ident, id) # Downloads and unpacks all licenses for a given source ID @@ -1594,6 +1596,7 @@ def InitLicenseData(src_id, dir, licenses, available_langs, end # Should have been named 'UpdateLicenseContentBasedOnSelectedLanguage' :-> + # @param licenses [ArgRef] a map $[ lang_code : filename ] def UpdateLicenseContent(licenses, id) # read the selected language @lic_lang = Convert.to_string( @@ -1710,8 +1713,7 @@ def product_license(id, tmpdir) log.info("License locales for product #{product_name.inspect}: #{locales.inspect}") locales.each do |locale| - license_locale = (Yast::UI.TextMode && locale.empty?) ? default_language : locale - license = Pkg.PrdGetLicenseToConfirm(product_name, license_locale) + license = Pkg.PrdGetLicenseToConfirm(product_name, locale) next if license.nil? || license.empty? found_license = true diff --git a/test/lib/widgets/product_license_translations_test.rb b/test/lib/widgets/product_license_translations_test.rb index 149856c96..772750ba2 100644 --- a/test/lib/widgets/product_license_translations_test.rb +++ b/test/lib/widgets/product_license_translations_test.rb @@ -17,6 +17,13 @@ require "y2packager/widgets/product_license_translations" require "y2packager/product" +RSpec::Matchers.define :array_not_including do |x| + match do |actual| + return false unless actual.is_a?(Array) + !actual.include?(x) + end +end + describe Y2Packager::Widgets::ProductLicenseTranslations do include_examples "CWM::CustomWidget" @@ -24,78 +31,60 @@ let(:language) { "de_DE" } let(:product) do - instance_double(Y2Packager::Product, license_locales: ["en_US", "ja"], license: "content") + instance_double( + Y2Packager::Product, + license_locales: ["en_US", "de_DE", "ja_JP"], + license: "content" + ) + end + + before do + allow(Yast::Language).to receive(:supported_language?).and_return(true) end describe "#contents" do it "includes a language selector" do expect(Y2Packager::Widgets::SimpleLanguageSelection).to receive(:new) - .with(product.license_locales, language) widget.contents end it "includes the product license text" do expect(Y2Packager::Widgets::ProductLicenseContent).to receive(:new) - .with(product, language) widget.contents end - context "when running on textmode" do - let(:preselected) { "ja_JP" } - + context "when selected language cannot be displayed" do before do - allow(Yast::UI).to receive(:TextMode).and_return(true) - allow(Yast::Language).to receive(:preselected).and_return(preselected) - allow(Yast::Stage).to receive(:initial).and_return(initial) + allow(Yast::Language).to receive(:supported_language?) + .with(language).and_return(false) end - context "on installation" do - let(:initial) { true } - - it "the language selector includes only the preselected language" do - expect(Y2Packager::Widgets::SimpleLanguageSelection).to receive(:new) - .with([preselected], preselected) - widget.contents - end - - it "shows the product license in the preselected language" do - expect(Y2Packager::Widgets::ProductLicenseContent).to receive(:new) - .with(product, preselected) - widget.contents - end - - context "when there is no translation for the preselected language" do - let(:preselected) { "hu_HU" } - - it "the language selector includes only 'english'" do - expect(Y2Packager::Widgets::SimpleLanguageSelection).to receive(:new) - .with(["en_US"], "en_US") - widget.contents - end - - it "shows the product license in 'english'" do - expect(Y2Packager::Widgets::ProductLicenseContent).to receive(:new) - .with(product, "en_US") - widget.contents - end - end + it "does not include it in the language selector" do + expect(Y2Packager::Widgets::SimpleLanguageSelection).to receive(:new) + .with(array_not_including("de_DE"), anything) + widget.contents + end + + it "shows the product license in the default language (AmE)" do + expect(Y2Packager::Widgets::ProductLicenseContent).to receive(:new) + .with(product, "en_US") + widget.contents + end + end + + context "when there is no translation for the given language" do + let(:language) { "hu_HU" } + + it "does not include it in the language selector" do + expect(Y2Packager::Widgets::SimpleLanguageSelection).to receive(:new) + .with(array_not_including("hu_HU"), anything) + widget.contents end - context "on the installed system" do - let(:initial) { false } - let(:language) { "ja_JP" } - - it "the language selector includes only the default language" do - expect(Y2Packager::Widgets::SimpleLanguageSelection).to receive(:new) - .with([language], language) - widget.contents - end - - it "shows the product license in the default language" do - expect(Y2Packager::Widgets::ProductLicenseContent).to receive(:new) - .with(product, language) - widget.contents - end + it "shows the product license in the default language (AmE)" do + expect(Y2Packager::Widgets::ProductLicenseContent).to receive(:new) + .with(product, "en_US") + widget.contents end end end diff --git a/test/product_license_test.rb b/test/product_license_test.rb index d000f5906..a8f7ccc3f 100755 --- a/test/product_license_test.rb +++ b/test/product_license_test.rb @@ -13,71 +13,23 @@ subject { Yast::ProductLicense } describe "#DisplayLicenseDialogWithTitle (partial test)" do - context "when running in text mode" do - let(:langs) { ["en_US", "ja"] } - let(:lang) { "es_ES" } - let(:licenses) { Yast.arg_ref("en_US" => "en_US license") } - let(:license_id) { "id" } - let(:preselected) { "ja_JP" } + let(:langs) { ["en_US", "ja"] } + let(:lang) { "en_US" } + let(:licenses) { Yast.arg_ref("en_US" => "en_US license") } + let(:license_id) { "id" } - before do - allow(Yast::Language).to receive(:GetLanguagesMap).and_return({}) - allow(Yast::UI).to receive(:TextMode).and_return(true) - allow(Yast::Language).to receive(:preselected).and_return(preselected) - allow(Yast::Stage).to receive(:initial).and_return(initial) - allow(Yast::Language).to receive(:language).and_return(lang) - end - - context "on the installation" do - let(:initial) { true } - - it "uses the preselected language" do - expect(subject).to receive(:GetLicenseDialog) - .with(["ja"], "ja", anything, license_id, false) - .and_call_original - subject.DisplayLicenseDialogWithTitle( - langs, "Back", lang, licenses, license_id, "License" - ) - end - - context "when there is no translation for the preselected language" do - let(:preselected) { "es_ES" } - - it "falls back to 'english'" do - expect(subject).to receive(:GetLicenseDialog) - .with(["en_US"], "en_US", anything, license_id, false) - .and_call_original - subject.DisplayLicenseDialogWithTitle( - langs, "Back", lang, licenses, license_id, "License" - ) - end - end - end - - context "on the installed system" do - let(:initial) { false } - let(:lang) { "ja_JP" } - - it "uses the default language" do - expect(subject).to receive(:GetLicenseDialog) - .with(["ja"], "ja", anything, license_id, false) - subject.DisplayLicenseDialogWithTitle( - langs, "Back", lang, licenses, license_id, "License" - ) - end - - context "when there is no translation for the default language" do - let(:lang) { "es_ES" } + before do + allow(Yast::Language).to receive(:GetLanguagesMap).and_return({}) + allow(Yast::Language).to receive(:supported_language?).and_return(true) + end - it "falls back to 'english'" do - expect(subject).to receive(:GetLicenseDialog) - .with(["en_US"], "en_US", anything, license_id, false) - subject.DisplayLicenseDialogWithTitle( - langs, "Back", lang, licenses, license_id, "License" - ) - end - end - end + it "works" do + expect(subject).to receive(:GetLicenseDialog) + .with(["en_US", "ja"], "en_US", anything, license_id, false) + .and_call_original + subject.DisplayLicenseDialogWithTitle( + langs, "Back", lang, licenses, license_id, "License" + ) end end