diff --git a/Library/Homebrew/cask/lib/hbc/auditor.rb b/Library/Homebrew/cask/lib/hbc/auditor.rb index 6b0c1c4764373..ee1b50938b75b 100644 --- a/Library/Homebrew/cask/lib/hbc/auditor.rb +++ b/Library/Homebrew/cask/lib/hbc/auditor.rb @@ -1,6 +1,23 @@ module Hbc class Auditor def self.audit(cask, audit_download: false, check_token_conflicts: false) + saved_languages = MacOS.instance_variable_get(:@languages) + + if languages_blocks = cask.instance_variable_get(:@dsl).instance_variable_get(:@language_blocks) + languages_blocks.keys.each do |languages| + ohai "Auditing language: #{languages.map { |lang| "'#{lang}'" }.join(", ")}" + MacOS.instance_variable_set(:@languages, languages) + audit_cask_instance(Hbc.load(cask.sourcefile_path), audit_download, check_token_conflicts) + CLI::Cleanup.run(cask.token) if audit_download + end + else + audit_cask_instance(cask, audit_download, check_token_conflicts) + end + ensure + MacOS.instance_variable_set(:@languages, saved_languages) + end + + def self.audit_cask_instance(cask, audit_download, check_token_conflicts) download = audit_download && Download.new(cask) audit = Audit.new(cask, download: download, check_token_conflicts: check_token_conflicts) diff --git a/Library/Homebrew/cask/lib/hbc/cask.rb b/Library/Homebrew/cask/lib/hbc/cask.rb index 756b05b83fcb7..1e2056efc705e 100644 --- a/Library/Homebrew/cask/lib/hbc/cask.rb +++ b/Library/Homebrew/cask/lib/hbc/cask.rb @@ -11,7 +11,10 @@ def initialize(token, sourcefile_path: nil, dsl: nil, &block) @token = token @sourcefile_path = sourcefile_path @dsl = dsl || DSL.new(@token) - @dsl.instance_eval(&block) if block_given? + if block_given? + @dsl.instance_eval(&block) + @dsl.language_eval + end end DSL::DSL_METHODS.each do |method_name| diff --git a/Library/Homebrew/cask/lib/hbc/cli.rb b/Library/Homebrew/cask/lib/hbc/cli.rb index 3f67e131ddc1c..f637ae7af5f72 100644 --- a/Library/Homebrew/cask/lib/hbc/cli.rb +++ b/Library/Homebrew/cask/lib/hbc/cli.rb @@ -179,6 +179,10 @@ def self.nice_listing(cask_list) def self.parser # If you modify these arguments, please update USAGE.md @parser ||= OptionParser.new do |opts| + opts.on("--language STRING") do + # handled in OS::Mac + end + OPTIONS.each do |option, method| opts.on("#{option}" "PATH", Pathname) do |path| Hbc.public_send(method, path) diff --git a/Library/Homebrew/cask/lib/hbc/dsl.rb b/Library/Homebrew/cask/lib/hbc/dsl.rb index 83c0bf1fb3316..8e0a7715af427 100644 --- a/Library/Homebrew/cask/lib/hbc/dsl.rb +++ b/Library/Homebrew/cask/lib/hbc/dsl.rb @@ -1,4 +1,5 @@ require "set" +require "locale" require "hbc/dsl/appcast" require "hbc/dsl/base" @@ -64,6 +65,7 @@ class DSL :depends_on, :gpg, :homepage, + :language, :license, :name, :sha256, @@ -98,6 +100,43 @@ def homepage(homepage = nil) @homepage ||= homepage end + def language(*args, default: false, &block) + if !args.empty? && block_given? + @language_blocks ||= {} + @language_blocks[args] = block + + return unless default + + unless @language_blocks.default.nil? + raise CaskInvalidError.new(token, "Only one default language may be defined") + end + + @language_blocks.default = block + else + language_eval + end + end + + def language_eval + return @language if instance_variable_defined?(:@language) + + if @language_blocks.nil? || @language_blocks.empty? + return @language = nil + end + + MacOS.languages.map(&Locale.method(:parse)).each do |locale| + key = @language_blocks.keys.detect { |strings| + strings.any? { |string| locale.include?(string) } + } + + next if key.nil? + + return @language = @language_blocks[key].call + end + + @language = @language_blocks.default.call + end + def url(*args, &block) url_given = !args.empty? || block_given? return @url unless url_given diff --git a/Library/Homebrew/cask/lib/hbc/dsl/base.rb b/Library/Homebrew/cask/lib/hbc/dsl/base.rb index ccf93dae9a213..20a3cec619baa 100644 --- a/Library/Homebrew/cask/lib/hbc/dsl/base.rb +++ b/Library/Homebrew/cask/lib/hbc/dsl/base.rb @@ -8,7 +8,7 @@ def initialize(cask, command = SystemCommand) @command = command end - def_delegators :@cask, :token, :version, :caskroom_path, :staged_path, :appdir + def_delegators :@cask, :token, :version, :caskroom_path, :staged_path, :appdir, :language def system_command(executable, options = {}) @command.run!(executable, options) diff --git a/Library/Homebrew/cask/spec/locale_spec.rb b/Library/Homebrew/cask/spec/locale_spec.rb new file mode 100644 index 0000000000000..98a2de9139469 --- /dev/null +++ b/Library/Homebrew/cask/spec/locale_spec.rb @@ -0,0 +1,72 @@ +require "spec_helper" +require "locale" + +describe Locale do + describe "::parse" do + it "parses a string in the correct format" do + expect(described_class.parse("zh")).to eql(described_class.new("zh", nil, nil)) + expect(described_class.parse("zh-CN")).to eql(described_class.new("zh", "CN", nil)) + expect(described_class.parse("zh-Hans")).to eql(described_class.new("zh", nil, "Hans")) + expect(described_class.parse("zh-CN-Hans")).to eql(described_class.new("zh", "CN", "Hans")) + end + + context "raises a ParserError when given" do + it "an empty string" do + expect{ described_class.parse("") }.to raise_error(Locale::ParserError) + end + + it "a string in a wrong format" do + expect { described_class.parse("zh_CN_Hans") }.to raise_error(Locale::ParserError) + expect { described_class.parse("zhCNHans") }.to raise_error(Locale::ParserError) + expect { described_class.parse("zh-CN_Hans") }.to raise_error(Locale::ParserError) + expect { described_class.parse("zhCN") }.to raise_error(Locale::ParserError) + expect { described_class.parse("zh_Hans") }.to raise_error(Locale::ParserError) + end + end + end + + describe "::new" do + it "raises an ArgumentError when all arguments are nil" do + expect { described_class.new(nil, nil, nil) }.to raise_error(ArgumentError) + end + + it "raises a ParserError when one of the arguments does not match the locale format" do + expect { described_class.new("ZH", nil, nil) }.to raise_error(Locale::ParserError) + expect { described_class.new(nil, "cn", nil) }.to raise_error(Locale::ParserError) + expect { described_class.new(nil, nil, "hans") }.to raise_error(Locale::ParserError) + end + end + + subject { described_class.new("zh", "CN", "Hans") } + + describe "#include?" do + it { is_expected.to include("zh") } + it { is_expected.to include("zh-CN") } + it { is_expected.to include("CN") } + it { is_expected.to include("CN-Hans") } + it { is_expected.to include("Hans") } + it { is_expected.to include("zh-CN-Hans") } + end + + describe "#eql?" do + subject { described_class.new("zh", "CN", "Hans") } + + context "all parts match" do + it { is_expected.to eql("zh-CN-Hans") } + it { is_expected.to eql(subject) } + end + + context "only some parts match" do + it { is_expected.to_not eql("zh") } + it { is_expected.to_not eql("zh-CN") } + it { is_expected.to_not eql("CN") } + it { is_expected.to_not eql("CN-Hans") } + it { is_expected.to_not eql("Hans") } + end + + it "does not raise if 'other' cannot be parsed" do + expect { subject.eql?("zh_CN_Hans") }.not_to raise_error + expect(subject.eql?("zh_CN_Hans")).to be false + end + end +end diff --git a/Library/Homebrew/cask/test/cask/dsl_test.rb b/Library/Homebrew/cask/test/cask/dsl_test.rb index ccf2f1a24816f..053eae1e1dd11 100644 --- a/Library/Homebrew/cask/test/cask/dsl_test.rb +++ b/Library/Homebrew/cask/test/cask/dsl_test.rb @@ -122,6 +122,56 @@ end end + describe "language stanza" do + it "allows multilingual casks" do + cask = lambda do + Hbc::Cask.new("cask-with-apps") do + language "zh" do + sha256 "abc123" + "zh-CN" + end + + language "en-US", default: true do + sha256 "xyz789" + "en-US" + end + + url "https://example.org/#{language}.zip" + end + end + + MacOS.stubs(languages: ["zh"]) + cask.call.language.must_equal "zh-CN" + cask.call.sha256.must_equal "abc123" + cask.call.url.to_s.must_equal "https://example.org/zh-CN.zip" + + MacOS.stubs(languages: ["zh-XX"]) + cask.call.language.must_equal "zh-CN" + cask.call.sha256.must_equal "abc123" + cask.call.url.to_s.must_equal "https://example.org/zh-CN.zip" + + MacOS.stubs(languages: ["en"]) + cask.call.language.must_equal "en-US" + cask.call.sha256.must_equal "xyz789" + cask.call.url.to_s.must_equal "https://example.org/en-US.zip" + + MacOS.stubs(languages: ["xx-XX"]) + cask.call.language.must_equal "en-US" + cask.call.sha256.must_equal "xyz789" + cask.call.url.to_s.must_equal "https://example.org/en-US.zip" + + MacOS.stubs(languages: ["xx-XX", "zh", "en"]) + cask.call.language.must_equal "zh-CN" + cask.call.sha256.must_equal "abc123" + cask.call.url.to_s.must_equal "https://example.org/zh-CN.zip" + + MacOS.stubs(languages: ["xx-XX", "en-US", "zh"]) + cask.call.language.must_equal "en-US" + cask.call.sha256.must_equal "xyz789" + cask.call.url.to_s.must_equal "https://example.org/en-US.zip" + end + end + describe "app stanza" do it "allows you to specify app stanzas" do cask = Hbc::Cask.new("cask-with-apps") do diff --git a/Library/Homebrew/locale.rb b/Library/Homebrew/locale.rb new file mode 100644 index 0000000000000..1aff33dae6c8b --- /dev/null +++ b/Library/Homebrew/locale.rb @@ -0,0 +1,68 @@ +class Locale + class ParserError < ::RuntimeError + end + + LANGUAGE_REGEX = /(?:[a-z]{2})/ + REGION_REGEX = /(?:[A-Z]{2})/ + SCRIPT_REGEX = /(?:[A-Z][a-z]{3})/ + + LOCALE_REGEX = /^(#{LANGUAGE_REGEX})?(?:(?:^|-)(#{REGION_REGEX}))?(?:(?:^|-)(#{SCRIPT_REGEX}))?$/ + + def self.parse(string) + language, region, script = string.to_s.scan(LOCALE_REGEX)[0] + + if language.nil? && region.nil? && script.nil? + raise ParserError, "'#{string}' cannot be parsed to a #{self.class}" + end + + new(language, region, script) + end + + attr_reader :language, :region, :script + + def initialize(language, region, script) + if language.nil? && region.nil? && script.nil? + raise ArgumentError, "#{self.class} cannot be empty" + end + + { + language: language, + region: region, + script: script, + }.each do |key, value| + next if value.nil? + + regex = self.class.const_get("#{key.upcase}_REGEX") + raise ParserError, "'#{value}' does not match #{regex}" unless value =~ regex + instance_variable_set(:"@#{key}", value) + end + + self + end + + def include?(other) + other = self.class.parse(other) unless other.is_a?(self.class) + + [:language, :region, :script].all? { |var| + if other.public_send(var).nil? + true + else + public_send(var) == other.public_send(var) + end + } + end + + def eql?(other) + other = self.class.parse(other) unless other.is_a?(self.class) + [:language, :region, :script].all? { |var| + public_send(var) == other.public_send(var) + } + rescue ParserError + false + end + alias == eql? + + def to_s + [@language, @region, @script].compact.join("-") + end +end diff --git a/Library/Homebrew/os/mac.rb b/Library/Homebrew/os/mac.rb index 0b01478253f1c..b2f0515a0a2bd 100644 --- a/Library/Homebrew/os/mac.rb +++ b/Library/Homebrew/os/mac.rb @@ -41,8 +41,24 @@ def cat version.to_sym end + def languages + return @languages unless @languages.nil? + + @languages = Utils.popen_read("defaults", "read", ".GlobalPreferences", "AppleLanguages").scan(/[^ \n"(),]+/) + + if ENV["HOMEBREW_LANGUAGES"] + @languages = ENV["HOMEBREW_LANGUAGES"].split(",") + @languages + end + + if ARGV.value("language") + @languages = ARGV.value("language").split(",") + @languages + end + + @languages = @languages.uniq + end + def language - @language ||= Utils.popen_read("defaults", "read", ".GlobalPreferences", "AppleLanguages").delete(" \n\"()").sub(/,.*/, "") + languages.first end def active_developer_dir diff --git a/Library/Homebrew/test/test_os_mac_language.rb b/Library/Homebrew/test/test_os_mac_language.rb index 2cdd50917da18..709913000bb98 100644 --- a/Library/Homebrew/test/test_os_mac_language.rb +++ b/Library/Homebrew/test/test_os_mac_language.rb @@ -2,7 +2,15 @@ require "os/mac" class OSMacLanguageTests < Homebrew::TestCase + LANGUAGE_REGEX = /\A[a-z]{2}(-[A-Z]{2})?(-[A-Z][a-z]{3})?\Z/ + + def test_languages_format + OS::Mac.languages.each do |language| + assert_match LANGUAGE_REGEX, language + end + end + def test_language_format - assert_match(/\A[a-z]{2}(-[A-Z]{2})?\Z/, OS::Mac.language) + assert_match LANGUAGE_REGEX, OS::Mac.language end end