From 703e5b2b93fd300d79adad570754514b8443fad0 Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Tue, 10 Aug 2021 18:53:57 +0200
Subject: [PATCH 01/10] Initial implementation for Locale models (WIP)

Still requires:
 - Completing the list of ALL_KNOWN_LOCALES
 - Enabling from_ios and from_app_store family of tests once we have the values
---
 lib/fastlane/plugin/wpmreleasetoolkit.rb      |   2 +-
 .../wpmreleasetoolkit/models/locales.rb       | 129 ++++++++++++++++++
 spec/locales_spec.rb                          | 105 ++++++++++++++
 3 files changed, 235 insertions(+), 1 deletion(-)
 create mode 100644 lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
 create mode 100644 spec/locales_spec.rb

diff --git a/lib/fastlane/plugin/wpmreleasetoolkit.rb b/lib/fastlane/plugin/wpmreleasetoolkit.rb
index dad7c7064..a1128823c 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit.rb
@@ -4,7 +4,7 @@ module Fastlane
   module Wpmreleasetoolkit
     # Return all .rb files inside the "actions" and "helper" directory
     def self.all_classes
-      Dir[File.expand_path('**/{actions,helper}/**/*.rb', File.dirname(__FILE__))]
+      Dir[File.expand_path('**/{actions,helper,models}/**/*.rb', File.dirname(__FILE__))]
     end
   end
 end
diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
new file mode 100644
index 000000000..9381d8fd6
--- /dev/null
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
@@ -0,0 +1,129 @@
+module Fastlane
+  Locale = Struct.new(:glotpress, :android, :google_play, :ios, :app_store, keyword_init: true) do
+    def self.[](code)
+      Locales[code].first
+    end
+  end
+
+  class Locales
+
+    ###################
+    ## Constants
+    ALL_KNOWN_LOCALES = [
+      Locale.new(glotpress: "ar",    android: "ar",     google_play: "ar"   ),
+      Locale.new(glotpress: "de",    android: "de",     google_play: "de-DE"),
+      Locale.new(glotpress: "en-gb", android: "en-rGB", google_play: "en-US"),
+      Locale.new(glotpress: "es",    android: "es",     google_play: "es-ES"),
+      Locale.new(glotpress: "fr-ca",    android: "fr-rCA", google_play: "fr-CA"),
+      Locale.new(glotpress: "fr",    android: "fr",     google_play: "fr-FR"),
+      Locale.new(glotpress: "he",    android: "he",     google_play: "iw-IL"),
+      Locale.new(glotpress: "id",    android: "id",     google_play: "id"   ),
+      Locale.new(glotpress: "it",    android: "it",     google_play: "it-IT"),
+      Locale.new(glotpress: "ja",    android: "ja",     google_play: "ja-JP"),
+      Locale.new(glotpress: "ko",    android: "ko",     google_play: "ko-KR"),
+      Locale.new(glotpress: "nl",    android: "nl",     google_play: "nl-NL"),
+      Locale.new(glotpress: "pl",    android: "pl",     google_play: "pl-PL"),
+      Locale.new(glotpress: "pt-br", android: "pt-rBR", google_play: "pt-BR"),
+      Locale.new(glotpress: "ru",    android: "ru",     google_play: "ru-RU"),
+      Locale.new(glotpress: "sr",    android: "sr",     google_play: "sr"   ),
+      Locale.new(glotpress: "sv",    android: "sv",     google_play: "sv-SE"),
+      Locale.new(glotpress: "th",    android: "th",     google_play: "th"   ),
+      Locale.new(glotpress: "tr",    android: "tr",     google_play: "tr-TR"),
+      Locale.new(glotpress: "vi",    android: "vi",     google_play: "vi"   ),
+      Locale.new(glotpress: "zh-cn", android: "zh-rCN", google_play: "zh-CN"),
+      Locale.new(glotpress: "zh-tw", android: "zh-rTW", google_play: "zh-TW"),
+      Locale.new(glotpress: "az",    android: "az"),
+      Locale.new(glotpress: "el",    android: "el"),
+      Locale.new(glotpress: "es-mx", android: "es-rMX"),
+      Locale.new(glotpress: "es-cl", android: "es-rCL"),
+      Locale.new(glotpress: "gd",    android: "gd"),
+      Locale.new(glotpress: "hi",    android: "hi"),
+      Locale.new(glotpress: "hu",    android: "hu"),
+      Locale.new(glotpress: "nb",    android: "nb"),
+      Locale.new(glotpress: "pl",    android: "pl"),
+      Locale.new(glotpress: "th",    android: "th"),
+      Locale.new(glotpress: "uz",    android: "uz"),
+      Locale.new(glotpress: "zh-tw", android: "zh-rHK"),
+      Locale.new(glotpress: "eu",    android: "eu"),
+      Locale.new(glotpress: "ro",    android: "ro"),
+      Locale.new(glotpress: "mk",    android: "mk"),
+      Locale.new(glotpress: "en-au", android: "en-rAU"),
+      Locale.new(glotpress: "sr",    android: "sr"),
+      Locale.new(glotpress: "sk",    android: "sk"),
+      Locale.new(glotpress: "cy",    android: "cy"),
+      Locale.new(glotpress: "da",    android: "da"),
+      Locale.new(glotpress: "bg",    android: "bg"),
+      Locale.new(glotpress: "sq",    android: "sq"),
+      Locale.new(glotpress: "hr",    android: "hr"),
+      Locale.new(glotpress: "cs",    android: "cs"),
+      Locale.new(glotpress: "pt-br", android: "pt-rBR"),
+      Locale.new(glotpress: "en-ca", android: "en-rCA"),
+      Locale.new(glotpress: "ms",    android: "ms"),
+      Locale.new(glotpress: "es-ve", android: "es-rVE"),
+      Locale.new(glotpress: "gl",    android: "gl"),
+      Locale.new(glotpress: "is",    android: "is"),
+      Locale.new(glotpress: "es-co", android: "es-rCO"),
+      Locale.new(glotpress: "kmr",   android: "kmr")
+    ].freeze
+
+    MAG16_GP_CODES = %w[ar de es fr he id it ja ko nl pt-br ru sv tr zh-cn zh-tw].freeze
+
+    ###################
+    ## Static Methods
+
+    class << self
+
+      # @return [Array<Locale>] Array of all the known locales
+      #
+      def all
+        ALL_KNOWN_LOCALES
+      end
+
+      # Define from_glotpress(code_or_list), from_android(code_or_list) … methods
+      #
+      # @param [Array<String>, String] list of locale codes to search for, or single value for single result
+      # @return [Array<Locale>, Locale] list of found locales (empty if none found), or single locale if a single value was passed (or nil if not found)
+      #
+      %i[glotpress android google_play ios app_store].each do |key|
+        define_method("from_#{key}") { |args| search(key, args) }
+      end
+
+      # Return an Array<Locale> based on glotpress locale codes
+      #
+      # @note If you need a single locale, you can use Locale[code] instead of Locales[code]
+      # @param [String..., Array<String>] Arbitrary list of strings, either passed as a single array parameter, or as a vararg list of params
+      # @return [Array<Locale>] The found locales.
+      #
+      def [](*list)
+        # If we passed an Array, `*list` will make it an Array<Array<String>>, so taking `list.first` in those cases to go back to Array<String>
+        list = list.first if list.count == 1 && list.first.is_a?(Array)
+        from_glotpress(list)
+      end
+
+      # Return the subset of the 16 locales most of our apps are localized 100% (what we call the "Magnificent 16")
+      #
+      # @return [Array<Locale>] List of the Mag16 locales
+      def mag16
+        from_glotpress(MAG16_GP_CODES)
+      end
+
+      ###################
+
+      private
+
+      # Search the known locales for just the ones having the provided locale code, where the codes are expressed using the standard for the given key
+      def search(key, code_or_list)
+        if code_or_list.is_a?(Array)
+          code_or_list.map { |code| search(key, code) }
+        else # String
+          raise 'The locale code should not contain spaces. Did you accidentally use `%[]` instead of `%w[]` at call site?' if code_or_list.include?(' ')
+          ALL_KNOWN_LOCALES.find { |locale| locale.send(key) == code_or_list } || not_found(code_or_list, key)
+        end
+      end
+
+      def not_found(code, key)
+        raise "Unknown locale for #{key} code '#{code}'"
+      end
+    end
+  end
+end
diff --git a/spec/locales_spec.rb b/spec/locales_spec.rb
new file mode 100644
index 000000000..84a32cac4
--- /dev/null
+++ b/spec/locales_spec.rb
@@ -0,0 +1,105 @@
+require 'spec_helper'
+
+describe Fastlane::Locale do
+  it 'returns a single Locale if one was found' do
+    locale = Fastlane::Locale['fr']
+    expect(locale).to be_instance_of(Fastlane::Locale)
+    expect(locale.glotpress).to eq('fr')
+  end
+
+  it 'raises if no locale was found for a given code' do
+    expect {
+      Fastlane::Locale['invalidcode']
+    }.to raise_error(RuntimeError, "Unknown locale for glotpress code 'invalidcode'")
+  end
+end
+
+describe Fastlane::Locales do
+  shared_examples 'from_xxx' do |key, fr_code, pt_code|
+    let(:method_sym) { "from_#{key}".to_sym }
+    it 'can find a locale from a single code' do
+      fr_locale = Fastlane::Locales.send(method_sym, fr_code)
+      expect(fr_locale).to be_instance_of(Fastlane::Locale)
+      expect(fr_locale.glotpress).to eq('fr')
+      expect(fr_locale.android).to eq('fr')
+      expect(fr_locale.google_play).to eq('fr-FR')
+    end
+
+    it 'can find locales from a multiple codes' do
+      locales = Fastlane::Locales.send(method_sym, [fr_code, pt_code])
+      expect(locales).to be_instance_of(Array)
+
+      expect(locales[0]).to be_instance_of(Fastlane::Locale)
+      expect(locales[0].glotpress).to eq('fr')
+
+      expect(locales[1]).to be_instance_of(Fastlane::Locale)
+      expect(locales[1].glotpress).to eq('pt-br')
+    end
+
+    it 'raises if one of the locale codes passed was not found' do
+      expect {
+        Fastlane::Locales.send(method_sym, [fr_code, 'invalidcode', 'pt-br'])
+      }.to raise_error(RuntimeError, "Unknown locale for #{key} code 'invalidcode'")
+    end
+  end
+
+  describe 'from_glotpress' do
+    include_examples 'from_xxx', :glotpress, 'fr', 'pt-br'
+  end
+
+  describe 'from_android' do
+    include_examples 'from_xxx', :android, 'fr', 'pt-rBR'
+  end
+
+  describe 'from_google_play' do
+    include_examples 'from_xxx', :google_play, 'fr-FR', 'pt-BR'
+  end
+
+  # @TODO: from_ios, from_app_store
+
+  describe 'subscript [] operator' do
+    it 'returns an Array<Locale> even if a single one was passed' do
+      locales = Fastlane::Locales['fr']
+      expect(locales).to be_instance_of(Array)
+      expect(locales.count).to equal(1)
+      expect(locales[0].glotpress).to eq('fr')
+    end
+
+    it 'returns an Array<Locale> if a list of vararg codes was passed' do
+      locales = Fastlane::Locales['fr', 'pt-br']
+      expect(locales).to be_instance_of(Array)
+      expect(locales.count).to equal(2)
+      expect(locales[0]).to be_instance_of(Fastlane::Locale)
+      expect(locales[0].glotpress).to eq('fr')
+      expect(locales[1]).to be_instance_of(Fastlane::Locale)
+      expect(locales[1].glotpress).to eq('pt-br')
+    end
+
+    it 'returns an Array<Locale> if an Array<String> of codes was passed' do
+      list = %w[fr pt-br]
+      locales = Fastlane::Locales[list]
+      expect(locales).to be_instance_of(Array)
+      expect(locales.count).to equal(2)
+      expect(locales[0]).to be_instance_of(Fastlane::Locale)
+      expect(locales[0].glotpress).to eq('fr')
+      expect(locales[1]).to be_instance_of(Fastlane::Locale)
+      expect(locales[1].glotpress).to eq('pt-br')
+    end
+  end
+
+  it 'returns exactly 16 Mag16 locales' do
+    expect(Fastlane::Locales.mag16.count).to eq(16)
+  end
+
+  it 'is easy to do Locale subset intersections' do
+    mag16_except_pt = Fastlane::Locales.mag16 - Fastlane::Locales['pt-br']
+    expect(mag16_except_pt.count).to equal(15)
+    expect(mag16_except_pt.find { |l| l.glotpress == 'pt-br' }).to be_nil
+    expect(mag16_except_pt.find { |l| l.glotpress == 'fr' }).to_not be_nil
+  end
+
+  it 'can convert a Locale to a hash' do
+    h = Fastlane::Locale['fr'].to_h
+    expect(h).to eq({ glotpress: 'fr', android: 'fr', google_play: 'fr-FR', ios: nil, app_store: nil })
+  end
+end

From 0bcd9619f8d0c50fa90030fffa22a659a7c21fd0 Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Tue, 10 Aug 2021 19:13:44 +0200
Subject: [PATCH 02/10] Fix rubocop violations

---
 .../wpmreleasetoolkit/models/locales.rb       | 111 +++++++++---------
 spec/locales_spec.rb                          |  10 +-
 2 files changed, 60 insertions(+), 61 deletions(-)

diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
index 9381d8fd6..4588c160f 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
@@ -6,64 +6,63 @@ def self.[](code)
   end
 
   class Locales
-
     ###################
     ## Constants
     ALL_KNOWN_LOCALES = [
-      Locale.new(glotpress: "ar",    android: "ar",     google_play: "ar"   ),
-      Locale.new(glotpress: "de",    android: "de",     google_play: "de-DE"),
-      Locale.new(glotpress: "en-gb", android: "en-rGB", google_play: "en-US"),
-      Locale.new(glotpress: "es",    android: "es",     google_play: "es-ES"),
-      Locale.new(glotpress: "fr-ca",    android: "fr-rCA", google_play: "fr-CA"),
-      Locale.new(glotpress: "fr",    android: "fr",     google_play: "fr-FR"),
-      Locale.new(glotpress: "he",    android: "he",     google_play: "iw-IL"),
-      Locale.new(glotpress: "id",    android: "id",     google_play: "id"   ),
-      Locale.new(glotpress: "it",    android: "it",     google_play: "it-IT"),
-      Locale.new(glotpress: "ja",    android: "ja",     google_play: "ja-JP"),
-      Locale.new(glotpress: "ko",    android: "ko",     google_play: "ko-KR"),
-      Locale.new(glotpress: "nl",    android: "nl",     google_play: "nl-NL"),
-      Locale.new(glotpress: "pl",    android: "pl",     google_play: "pl-PL"),
-      Locale.new(glotpress: "pt-br", android: "pt-rBR", google_play: "pt-BR"),
-      Locale.new(glotpress: "ru",    android: "ru",     google_play: "ru-RU"),
-      Locale.new(glotpress: "sr",    android: "sr",     google_play: "sr"   ),
-      Locale.new(glotpress: "sv",    android: "sv",     google_play: "sv-SE"),
-      Locale.new(glotpress: "th",    android: "th",     google_play: "th"   ),
-      Locale.new(glotpress: "tr",    android: "tr",     google_play: "tr-TR"),
-      Locale.new(glotpress: "vi",    android: "vi",     google_play: "vi"   ),
-      Locale.new(glotpress: "zh-cn", android: "zh-rCN", google_play: "zh-CN"),
-      Locale.new(glotpress: "zh-tw", android: "zh-rTW", google_play: "zh-TW"),
-      Locale.new(glotpress: "az",    android: "az"),
-      Locale.new(glotpress: "el",    android: "el"),
-      Locale.new(glotpress: "es-mx", android: "es-rMX"),
-      Locale.new(glotpress: "es-cl", android: "es-rCL"),
-      Locale.new(glotpress: "gd",    android: "gd"),
-      Locale.new(glotpress: "hi",    android: "hi"),
-      Locale.new(glotpress: "hu",    android: "hu"),
-      Locale.new(glotpress: "nb",    android: "nb"),
-      Locale.new(glotpress: "pl",    android: "pl"),
-      Locale.new(glotpress: "th",    android: "th"),
-      Locale.new(glotpress: "uz",    android: "uz"),
-      Locale.new(glotpress: "zh-tw", android: "zh-rHK"),
-      Locale.new(glotpress: "eu",    android: "eu"),
-      Locale.new(glotpress: "ro",    android: "ro"),
-      Locale.new(glotpress: "mk",    android: "mk"),
-      Locale.new(glotpress: "en-au", android: "en-rAU"),
-      Locale.new(glotpress: "sr",    android: "sr"),
-      Locale.new(glotpress: "sk",    android: "sk"),
-      Locale.new(glotpress: "cy",    android: "cy"),
-      Locale.new(glotpress: "da",    android: "da"),
-      Locale.new(glotpress: "bg",    android: "bg"),
-      Locale.new(glotpress: "sq",    android: "sq"),
-      Locale.new(glotpress: "hr",    android: "hr"),
-      Locale.new(glotpress: "cs",    android: "cs"),
-      Locale.new(glotpress: "pt-br", android: "pt-rBR"),
-      Locale.new(glotpress: "en-ca", android: "en-rCA"),
-      Locale.new(glotpress: "ms",    android: "ms"),
-      Locale.new(glotpress: "es-ve", android: "es-rVE"),
-      Locale.new(glotpress: "gl",    android: "gl"),
-      Locale.new(glotpress: "is",    android: "is"),
-      Locale.new(glotpress: "es-co", android: "es-rCO"),
-      Locale.new(glotpress: "kmr",   android: "kmr")
+      Locale.new(glotpress: 'ar',    android: 'ar',     google_play: 'ar'),
+      Locale.new(glotpress: 'de',    android: 'de',     google_play: 'de-DE'),
+      Locale.new(glotpress: 'en-gb', android: 'en-rGB', google_play: 'en-US'),
+      Locale.new(glotpress: 'es',    android: 'es',     google_play: 'es-ES'),
+      Locale.new(glotpress: 'fr-ca', android: 'fr-rCA', google_play: 'fr-CA'),
+      Locale.new(glotpress: 'fr',    android: 'fr',     google_play: 'fr-FR'),
+      Locale.new(glotpress: 'he',    android: 'he',     google_play: 'iw-IL'),
+      Locale.new(glotpress: 'id',    android: 'id',     google_play: 'id'),
+      Locale.new(glotpress: 'it',    android: 'it',     google_play: 'it-IT'),
+      Locale.new(glotpress: 'ja',    android: 'ja',     google_play: 'ja-JP'),
+      Locale.new(glotpress: 'ko',    android: 'ko',     google_play: 'ko-KR'),
+      Locale.new(glotpress: 'nl',    android: 'nl',     google_play: 'nl-NL'),
+      Locale.new(glotpress: 'pl',    android: 'pl',     google_play: 'pl-PL'),
+      Locale.new(glotpress: 'pt-br', android: 'pt-rBR', google_play: 'pt-BR'),
+      Locale.new(glotpress: 'ru',    android: 'ru',     google_play: 'ru-RU'),
+      Locale.new(glotpress: 'sr',    android: 'sr',     google_play: 'sr'),
+      Locale.new(glotpress: 'sv',    android: 'sv',     google_play: 'sv-SE'),
+      Locale.new(glotpress: 'th',    android: 'th',     google_play: 'th'),
+      Locale.new(glotpress: 'tr',    android: 'tr',     google_play: 'tr-TR'),
+      Locale.new(glotpress: 'vi',    android: 'vi',     google_play: 'vi'),
+      Locale.new(glotpress: 'zh-cn', android: 'zh-rCN', google_play: 'zh-CN'),
+      Locale.new(glotpress: 'zh-tw', android: 'zh-rTW', google_play: 'zh-TW'),
+      Locale.new(glotpress: 'az',    android: 'az'),
+      Locale.new(glotpress: 'el',    android: 'el'),
+      Locale.new(glotpress: 'es-mx', android: 'es-rMX'),
+      Locale.new(glotpress: 'es-cl', android: 'es-rCL'),
+      Locale.new(glotpress: 'gd',    android: 'gd'),
+      Locale.new(glotpress: 'hi',    android: 'hi'),
+      Locale.new(glotpress: 'hu',    android: 'hu'),
+      Locale.new(glotpress: 'nb',    android: 'nb'),
+      Locale.new(glotpress: 'pl',    android: 'pl'),
+      Locale.new(glotpress: 'th',    android: 'th'),
+      Locale.new(glotpress: 'uz',    android: 'uz'),
+      Locale.new(glotpress: 'zh-tw', android: 'zh-rHK'),
+      Locale.new(glotpress: 'eu',    android: 'eu'),
+      Locale.new(glotpress: 'ro',    android: 'ro'),
+      Locale.new(glotpress: 'mk',    android: 'mk'),
+      Locale.new(glotpress: 'en-au', android: 'en-rAU'),
+      Locale.new(glotpress: 'sr',    android: 'sr'),
+      Locale.new(glotpress: 'sk',    android: 'sk'),
+      Locale.new(glotpress: 'cy',    android: 'cy'),
+      Locale.new(glotpress: 'da',    android: 'da'),
+      Locale.new(glotpress: 'bg',    android: 'bg'),
+      Locale.new(glotpress: 'sq',    android: 'sq'),
+      Locale.new(glotpress: 'hr',    android: 'hr'),
+      Locale.new(glotpress: 'cs',    android: 'cs'),
+      Locale.new(glotpress: 'pt-br', android: 'pt-rBR'),
+      Locale.new(glotpress: 'en-ca', android: 'en-rCA'),
+      Locale.new(glotpress: 'ms',    android: 'ms'),
+      Locale.new(glotpress: 'es-ve', android: 'es-rVE'),
+      Locale.new(glotpress: 'gl',    android: 'gl'),
+      Locale.new(glotpress: 'is',    android: 'is'),
+      Locale.new(glotpress: 'es-co', android: 'es-rCO'),
+      Locale.new(glotpress: 'kmr',   android: 'kmr'),
     ].freeze
 
     MAG16_GP_CODES = %w[ar de es fr he id it ja ko nl pt-br ru sv tr zh-cn zh-tw].freeze
@@ -72,7 +71,6 @@ class Locales
     ## Static Methods
 
     class << self
-
       # @return [Array<Locale>] Array of all the known locales
       #
       def all
@@ -117,6 +115,7 @@ def search(key, code_or_list)
           code_or_list.map { |code| search(key, code) }
         else # String
           raise 'The locale code should not contain spaces. Did you accidentally use `%[]` instead of `%w[]` at call site?' if code_or_list.include?(' ')
+
           ALL_KNOWN_LOCALES.find { |locale| locale.send(key) == code_or_list } || not_found(code_or_list, key)
         end
       end
diff --git a/spec/locales_spec.rb b/spec/locales_spec.rb
index 84a32cac4..e5576b0d1 100644
--- a/spec/locales_spec.rb
+++ b/spec/locales_spec.rb
@@ -8,9 +8,9 @@
   end
 
   it 'raises if no locale was found for a given code' do
-    expect {
+    expect do
       Fastlane::Locale['invalidcode']
-    }.to raise_error(RuntimeError, "Unknown locale for glotpress code 'invalidcode'")
+    end.to raise_error(RuntimeError, "Unknown locale for glotpress code 'invalidcode'")
   end
 end
 
@@ -37,9 +37,9 @@
     end
 
     it 'raises if one of the locale codes passed was not found' do
-      expect {
+      expect do
         Fastlane::Locales.send(method_sym, [fr_code, 'invalidcode', 'pt-br'])
-      }.to raise_error(RuntimeError, "Unknown locale for #{key} code 'invalidcode'")
+      end.to raise_error(RuntimeError, "Unknown locale for #{key} code 'invalidcode'")
     end
   end
 
@@ -95,7 +95,7 @@
     mag16_except_pt = Fastlane::Locales.mag16 - Fastlane::Locales['pt-br']
     expect(mag16_except_pt.count).to equal(15)
     expect(mag16_except_pt.find { |l| l.glotpress == 'pt-br' }).to be_nil
-    expect(mag16_except_pt.find { |l| l.glotpress == 'fr' }).to_not be_nil
+    expect(mag16_except_pt.find { |l| l.glotpress == 'fr' }).not_to be_nil
   end
 
   it 'can convert a Locale to a hash' do

From 6a883ad6e5c1d06890064b03be58569225c7058a Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Tue, 10 Aug 2021 19:26:17 +0200
Subject: [PATCH 03/10] Split specs in 2 files + use described_class

---
 .rubocop_todo.yml    |  2 ++
 spec/locale_spec.rb  | 20 ++++++++++++++++++++
 spec/locales_spec.rb | 35 ++++++++---------------------------
 3 files changed, 30 insertions(+), 27 deletions(-)
 create mode 100644 spec/locale_spec.rb

diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 677efef55..6d6b32fc0 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -142,6 +142,8 @@ RSpec/FilePath:
     - 'spec/ios_merge_translators_strings_spec.rb'
     - 'spec/release_notes_helper_spec.rb'
     - 'spec/check_localization_progress_spec.rb'
+    - 'spec/locale_spec.rb'
+    - 'spec/locales_spec.rb'
 
 # Offense count: 8
 # Cop supports --auto-correct.
diff --git a/spec/locale_spec.rb b/spec/locale_spec.rb
new file mode 100644
index 000000000..f2bac484c
--- /dev/null
+++ b/spec/locale_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Fastlane::Locale do
+  it 'returns a single Locale if one was found' do
+    locale = described_class['fr']
+    expect(locale).to be_instance_of(described_class)
+    expect(locale.glotpress).to eq('fr')
+  end
+
+  it 'raises if no locale was found for a given code' do
+    expect do
+      described_class['invalidcode']
+    end.to raise_error(RuntimeError, "Unknown locale for glotpress code 'invalidcode'")
+  end
+
+  it 'can convert a Locale to a hash' do
+    h = described_class['fr'].to_h
+    expect(h).to eq({ glotpress: 'fr', android: 'fr', google_play: 'fr-FR', ios: nil, app_store: nil })
+  end
+end
diff --git a/spec/locales_spec.rb b/spec/locales_spec.rb
index e5576b0d1..435cd1153 100644
--- a/spec/locales_spec.rb
+++ b/spec/locales_spec.rb
@@ -1,24 +1,10 @@
 require 'spec_helper'
 
-describe Fastlane::Locale do
-  it 'returns a single Locale if one was found' do
-    locale = Fastlane::Locale['fr']
-    expect(locale).to be_instance_of(Fastlane::Locale)
-    expect(locale.glotpress).to eq('fr')
-  end
-
-  it 'raises if no locale was found for a given code' do
-    expect do
-      Fastlane::Locale['invalidcode']
-    end.to raise_error(RuntimeError, "Unknown locale for glotpress code 'invalidcode'")
-  end
-end
-
 describe Fastlane::Locales do
   shared_examples 'from_xxx' do |key, fr_code, pt_code|
     let(:method_sym) { "from_#{key}".to_sym }
     it 'can find a locale from a single code' do
-      fr_locale = Fastlane::Locales.send(method_sym, fr_code)
+      fr_locale = described_class.send(method_sym, fr_code)
       expect(fr_locale).to be_instance_of(Fastlane::Locale)
       expect(fr_locale.glotpress).to eq('fr')
       expect(fr_locale.android).to eq('fr')
@@ -26,7 +12,7 @@
     end
 
     it 'can find locales from a multiple codes' do
-      locales = Fastlane::Locales.send(method_sym, [fr_code, pt_code])
+      locales = described_class.send(method_sym, [fr_code, pt_code])
       expect(locales).to be_instance_of(Array)
 
       expect(locales[0]).to be_instance_of(Fastlane::Locale)
@@ -38,7 +24,7 @@
 
     it 'raises if one of the locale codes passed was not found' do
       expect do
-        Fastlane::Locales.send(method_sym, [fr_code, 'invalidcode', 'pt-br'])
+        described_class.send(method_sym, [fr_code, 'invalidcode', 'pt-br'])
       end.to raise_error(RuntimeError, "Unknown locale for #{key} code 'invalidcode'")
     end
   end
@@ -59,14 +45,14 @@
 
   describe 'subscript [] operator' do
     it 'returns an Array<Locale> even if a single one was passed' do
-      locales = Fastlane::Locales['fr']
+      locales = described_class['fr']
       expect(locales).to be_instance_of(Array)
       expect(locales.count).to equal(1)
       expect(locales[0].glotpress).to eq('fr')
     end
 
     it 'returns an Array<Locale> if a list of vararg codes was passed' do
-      locales = Fastlane::Locales['fr', 'pt-br']
+      locales = described_class['fr', 'pt-br']
       expect(locales).to be_instance_of(Array)
       expect(locales.count).to equal(2)
       expect(locales[0]).to be_instance_of(Fastlane::Locale)
@@ -77,7 +63,7 @@
 
     it 'returns an Array<Locale> if an Array<String> of codes was passed' do
       list = %w[fr pt-br]
-      locales = Fastlane::Locales[list]
+      locales = described_class[list]
       expect(locales).to be_instance_of(Array)
       expect(locales.count).to equal(2)
       expect(locales[0]).to be_instance_of(Fastlane::Locale)
@@ -88,18 +74,13 @@
   end
 
   it 'returns exactly 16 Mag16 locales' do
-    expect(Fastlane::Locales.mag16.count).to eq(16)
+    expect(described_class.mag16.count).to eq(16)
   end
 
   it 'is easy to do Locale subset intersections' do
-    mag16_except_pt = Fastlane::Locales.mag16 - Fastlane::Locales['pt-br']
+    mag16_except_pt = described_class.mag16 - described_class['pt-br']
     expect(mag16_except_pt.count).to equal(15)
     expect(mag16_except_pt.find { |l| l.glotpress == 'pt-br' }).to be_nil
     expect(mag16_except_pt.find { |l| l.glotpress == 'fr' }).not_to be_nil
   end
-
-  it 'can convert a Locale to a hash' do
-    h = Fastlane::Locale['fr'].to_h
-    expect(h).to eq({ glotpress: 'fr', android: 'fr', google_play: 'fr-FR', ios: nil, app_store: nil })
-  end
 end

From b67733a9a402d40e675dfdee73e3cbc5dff85813 Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Tue, 10 Aug 2021 19:46:31 +0200
Subject: [PATCH 04/10] Improve YARD documention

---
 .../wpmreleasetoolkit/models/locales.rb       | 37 ++++++++++++++-----
 1 file changed, 27 insertions(+), 10 deletions(-)

diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
index 4588c160f..1175af66a 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
@@ -1,10 +1,19 @@
 module Fastlane
+  # Define a single Locale with the various locale codes depending on the representation needed
   Locale = Struct.new(:glotpress, :android, :google_play, :ios, :app_store, keyword_init: true) do
+    # Returns the Locale with the given glotpress locale code from the list of all known locales (`Locales.all`)
+    #
+    # @param [String] The glotpress locale code for the locale to fetch
+    # @return [Locale] The locale found
+    # @raise [RuntimeException] if the locale with given glotpress code is unknown
     def self.[](code)
       Locales[code].first
     end
   end
 
+  # A class with static methods to manipulate lists of locales.
+  # Exposes various `Array<Locale>` lists like all known locales, the Mag16,
+  # and convenience methods to turn list of Strings into list of Locales.
   class Locales
     ###################
     ## Constants
@@ -77,28 +86,36 @@ def all
         ALL_KNOWN_LOCALES
       end
 
-      # Define from_glotpress(code_or_list), from_android(code_or_list) … methods
+      # Define from_glotpress(code_or_list), from_android(code_or_list) … methods.
+      #
+      # Those can be used in the rare cases where you need to find locales via codes other than the glotpress ones,
+      # like searching by android locale code(s) or google_play locale code(s).
+      # In most cases, prefer using the `Locales[…]` method instead ()with glotpress locale codes).
       #
       # @param [Array<String>, String] list of locale codes to search for, or single value for single result
-      # @return [Array<Locale>, Locale] list of found locales (empty if none found), or single locale if a single value was passed (or nil if not found)
+      # @return [Array<Locale>, Locale] list of found locales, or single locale if a single value was passed
+      # @raise [RuntimeException] if at least one of the locale codes was unknown
       #
       %i[glotpress android google_play ios app_store].each do |key|
-        define_method("from_#{key}") { |args| search(key, args) }
+        define_method("from_#{key}") { |args| search!(key, args) }
       end
 
       # Return an Array<Locale> based on glotpress locale codes
       #
-      # @note If you need a single locale, you can use Locale[code] instead of Locales[code]
+      # @note If you need a single locale instead of an `Array<Locale>`, you can use Locale[code] instead of Locales[code]
+      #
       # @param [String..., Array<String>] Arbitrary list of strings, either passed as a single array parameter, or as a vararg list of params
       # @return [Array<Locale>] The found locales.
+      # @raise [RuntimeException] if at least one of the locale codes was unknown
       #
       def [](*list)
-        # If we passed an Array, `*list` will make it an Array<Array<String>>, so taking `list.first` in those cases to go back to Array<String>
+        # If we passed a variadic list of Strings, `*list` will make it a single `Array<String>` and we were already good to go.
+        # But if we passed an Array, `*list` will make it an Array<Array<String>> of one item; taking `list.first` will go back to Array<String>.
         list = list.first if list.count == 1 && list.first.is_a?(Array)
         from_glotpress(list)
       end
 
-      # Return the subset of the 16 locales most of our apps are localized 100% (what we call the "Magnificent 16")
+      # Return the subset of the 16 locales most of our apps are localized 100% (the ones we call the "Magnificent 16")
       #
       # @return [Array<Locale>] List of the Mag16 locales
       def mag16
@@ -110,17 +127,17 @@ def mag16
       private
 
       # Search the known locales for just the ones having the provided locale code, where the codes are expressed using the standard for the given key
-      def search(key, code_or_list)
+      def search!(key, code_or_list)
         if code_or_list.is_a?(Array)
-          code_or_list.map { |code| search(key, code) }
+          code_or_list.map { |code| search!(key, code) }
         else # String
           raise 'The locale code should not contain spaces. Did you accidentally use `%[]` instead of `%w[]` at call site?' if code_or_list.include?(' ')
 
-          ALL_KNOWN_LOCALES.find { |locale| locale.send(key) == code_or_list } || not_found(code_or_list, key)
+          ALL_KNOWN_LOCALES.find { |locale| locale.send(key) == code_or_list } || not_found!(code_or_list, key)
         end
       end
 
-      def not_found(code, key)
+      def not_found!(code, key)
         raise "Unknown locale for #{key} code '#{code}'"
       end
     end

From fdb429f260773e326b6d1638d8fdfa1029323b05 Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Tue, 10 Aug 2021 20:10:17 +0200
Subject: [PATCH 05/10] Introduce a couple of ios/app_store values for some
 Locales as a starting point + enable missing corresponding specs

---
 lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb | 8 ++++----
 spec/locale_spec.rb                                     | 2 +-
 spec/locales_spec.rb                                    | 9 ++++++++-
 3 files changed, 13 insertions(+), 6 deletions(-)

diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
index 1175af66a..e82956c40 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
@@ -23,7 +23,7 @@ class Locales
       Locale.new(glotpress: 'en-gb', android: 'en-rGB', google_play: 'en-US'),
       Locale.new(glotpress: 'es',    android: 'es',     google_play: 'es-ES'),
       Locale.new(glotpress: 'fr-ca', android: 'fr-rCA', google_play: 'fr-CA'),
-      Locale.new(glotpress: 'fr',    android: 'fr',     google_play: 'fr-FR'),
+      Locale.new(glotpress: 'fr',    android: 'fr',     google_play: 'fr-FR', ios: 'fr-FR', app_store: 'fr-FR'),
       Locale.new(glotpress: 'he',    android: 'he',     google_play: 'iw-IL'),
       Locale.new(glotpress: 'id',    android: 'id',     google_play: 'id'),
       Locale.new(glotpress: 'it',    android: 'it',     google_play: 'it-IT'),
@@ -31,15 +31,15 @@ class Locales
       Locale.new(glotpress: 'ko',    android: 'ko',     google_play: 'ko-KR'),
       Locale.new(glotpress: 'nl',    android: 'nl',     google_play: 'nl-NL'),
       Locale.new(glotpress: 'pl',    android: 'pl',     google_play: 'pl-PL'),
-      Locale.new(glotpress: 'pt-br', android: 'pt-rBR', google_play: 'pt-BR'),
+      Locale.new(glotpress: 'pt-br', android: 'pt-rBR', google_play: 'pt-BR', ios: 'pt-BR', app_store: 'pt-BR'),
       Locale.new(glotpress: 'ru',    android: 'ru',     google_play: 'ru-RU'),
       Locale.new(glotpress: 'sr',    android: 'sr',     google_play: 'sr'),
       Locale.new(glotpress: 'sv',    android: 'sv',     google_play: 'sv-SE'),
       Locale.new(glotpress: 'th',    android: 'th',     google_play: 'th'),
       Locale.new(glotpress: 'tr',    android: 'tr',     google_play: 'tr-TR'),
       Locale.new(glotpress: 'vi',    android: 'vi',     google_play: 'vi'),
-      Locale.new(glotpress: 'zh-cn', android: 'zh-rCN', google_play: 'zh-CN'),
-      Locale.new(glotpress: 'zh-tw', android: 'zh-rTW', google_play: 'zh-TW'),
+      Locale.new(glotpress: 'zh-cn', android: 'zh-rCN', google_play: 'zh-CN', ios: 'zh-Hans', app_store: 'zh-Hans'),
+      Locale.new(glotpress: 'zh-tw', android: 'zh-rTW', google_play: 'zh-TW', ios: 'zh-Hant', app_store: 'zh-Hant'),
       Locale.new(glotpress: 'az',    android: 'az'),
       Locale.new(glotpress: 'el',    android: 'el'),
       Locale.new(glotpress: 'es-mx', android: 'es-rMX'),
diff --git a/spec/locale_spec.rb b/spec/locale_spec.rb
index f2bac484c..d22ba63bd 100644
--- a/spec/locale_spec.rb
+++ b/spec/locale_spec.rb
@@ -15,6 +15,6 @@
 
   it 'can convert a Locale to a hash' do
     h = described_class['fr'].to_h
-    expect(h).to eq({ glotpress: 'fr', android: 'fr', google_play: 'fr-FR', ios: nil, app_store: nil })
+    expect(h).to eq({ glotpress: 'fr', android: 'fr', google_play: 'fr-FR', ios: 'fr-FR', app_store: 'fr-FR' })
   end
 end
diff --git a/spec/locales_spec.rb b/spec/locales_spec.rb
index 435cd1153..c88da05da 100644
--- a/spec/locales_spec.rb
+++ b/spec/locales_spec.rb
@@ -3,6 +3,7 @@
 describe Fastlane::Locales do
   shared_examples 'from_xxx' do |key, fr_code, pt_code|
     let(:method_sym) { "from_#{key}".to_sym }
+
     it 'can find a locale from a single code' do
       fr_locale = described_class.send(method_sym, fr_code)
       expect(fr_locale).to be_instance_of(Fastlane::Locale)
@@ -41,7 +42,13 @@
     include_examples 'from_xxx', :google_play, 'fr-FR', 'pt-BR'
   end
 
-  # @TODO: from_ios, from_app_store
+  describe 'from_ios' do
+    include_examples 'from_xxx', :ios, 'fr-FR', 'pt-BR'
+  end
+
+  describe 'from_app_store' do
+    include_examples 'from_xxx', :app_store, 'fr-FR', 'pt-BR'
+  end
 
   describe 'subscript [] operator' do
     it 'returns an Array<Locale> even if a single one was passed' do

From 7a15db8d15fe0284bb633da416510dc7c93be1ed Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Tue, 10 Aug 2021 20:13:34 +0200
Subject: [PATCH 06/10] Add a spec to test locale code formats are consistent

---
 spec/locales_spec.rb | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/spec/locales_spec.rb b/spec/locales_spec.rb
index c88da05da..0de4a87ca 100644
--- a/spec/locales_spec.rb
+++ b/spec/locales_spec.rb
@@ -80,6 +80,16 @@
     end
   end
 
+  it 'has only valid codes for known locales' do
+    described_class.all.each do |locale|
+      expect(locale.glotpress || 'xx').to match(/^[a-z]{2,3}(-[a-z]{2})?$/)
+      expect(locale.android || 'xx-rYY').to match(/^[a-z]{2,3}(-r[A-Z]{2})?$/)
+      expect(locale.google_play || 'xx-YY').to match(/^[a-z]{2,3}(-[A-Z]{2})?$/)
+      expect(locale.app_store || 'xx-Yy').to match(/^[a-z]{2,3}(-[A-Za-z]{2,4})?$/)
+      expect(locale.ios || 'xx-Yy').to match(/^[a-z]{2,3}(-[A-Za-z]{2,4})?$/)
+    end
+  end
+
   it 'returns exactly 16 Mag16 locales' do
     expect(described_class.mag16.count).to eq(16)
   end

From 58188a5cb878a650de2c15186b211936e5c894e8 Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Fri, 13 Aug 2021 18:21:31 +0200
Subject: [PATCH 07/10] typos

---
 lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb | 2 +-
 spec/locales_spec.rb                                    | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
index e82956c40..158b9d8fb 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
@@ -90,7 +90,7 @@ def all
       #
       # Those can be used in the rare cases where you need to find locales via codes other than the glotpress ones,
       # like searching by android locale code(s) or google_play locale code(s).
-      # In most cases, prefer using the `Locales[…]` method instead ()with glotpress locale codes).
+      # In most cases, prefer using the `Locales[…]` method instead (with glotpress locale codes).
       #
       # @param [Array<String>, String] list of locale codes to search for, or single value for single result
       # @return [Array<Locale>, Locale] list of found locales, or single locale if a single value was passed
diff --git a/spec/locales_spec.rb b/spec/locales_spec.rb
index 0de4a87ca..6b1dd9823 100644
--- a/spec/locales_spec.rb
+++ b/spec/locales_spec.rb
@@ -25,7 +25,7 @@
 
     it 'raises if one of the locale codes passed was not found' do
       expect do
-        described_class.send(method_sym, [fr_code, 'invalidcode', 'pt-br'])
+        described_class.send(method_sym, [fr_code, 'invalidcode', pt_code])
       end.to raise_error(RuntimeError, "Unknown locale for #{key} code 'invalidcode'")
     end
   end

From 5e80651f67f1ad36014cd302dd87e11ad0cdfc75 Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Thu, 16 Sep 2021 22:05:14 +0200
Subject: [PATCH 08/10] Fix git failures in rspecs

When run with a modern git version which supports `--initial-branch`, git init was run twice due to using `|` instead of expected `||` in the shell commands
---
 spec/configure_helper_spec.rb |  2 +-
 spec/git_helper_spec.rb       | 14 +++++++-------
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/spec/configure_helper_spec.rb b/spec/configure_helper_spec.rb
index c45833f1f..00931f77b 100644
--- a/spec/configure_helper_spec.rb
+++ b/spec/configure_helper_spec.rb
@@ -16,7 +16,7 @@
         # reasonable enough assumption to make for the real world usage of this
         # tool. Still, it would be nice to have proper handling of that
         # scenario at some point.
-        `git init --initial-branch main | git init`
+        `git init --initial-branch main || git init`
 
         expect(Fastlane::UI).to receive(:user_error!)
 
diff --git a/spec/git_helper_spec.rb b/spec/git_helper_spec.rb
index 4d8edf8a6..1b5ece305 100644
--- a/spec/git_helper_spec.rb
+++ b/spec/git_helper_spec.rb
@@ -26,12 +26,12 @@
   end
 
   it 'can detect a valid git repository' do
-    `git init --initial-branch main | git init`
+    `git init --initial-branch main || git init`
     expect(Fastlane::Helper::GitHelper.is_git_repo?).to be true
   end
 
   it 'can detect a valid git repository from a child folder' do
-    `git init --initial-branch main | git init`
+    `git init --initial-branch main || git init`
     `mkdir -p a/b`
     Dir.chdir('./a/b')
     expect(Fastlane::Helper::GitHelper.is_git_repo?).to be true
@@ -39,14 +39,14 @@
 
   it 'can detect a valid git repository when given a path' do
     Dir.mktmpdir do |dir|
-      `git -C #{dir} init --initial-branch main | git -C #{dir} init`
+      `git -C #{dir} init --initial-branch main || git -C #{dir} init`
       expect(Fastlane::Helper::GitHelper.is_git_repo?(path: dir)).to be true
     end
   end
 
   it 'can detect a valid git repository when given a child folder path' do
     Dir.mktmpdir do |dir|
-      `git -C #{dir} init --initial-branch main | git -C #{dir} init`
+      `git -C #{dir} init --initial-branch main || git -C #{dir} init`
       path = File.join(dir, 'a', 'b')
       `mkdir -p #{path}`
       expect(Fastlane::Helper::GitHelper.is_git_repo?(path: path)).to be true
@@ -54,13 +54,13 @@
   end
 
   it 'can detect a repository with Git-lfs enabled' do
-    `git init --initial-branch main | git init`
+    `git init --initial-branch main || git init`
     `git lfs install`
     expect(Fastlane::Helper::GitHelper.has_git_lfs?).to be true
   end
 
   it 'can detect a repository without Git-lfs enabled' do
-    `git init --initial-branch main | git init`
+    `git init --initial-branch main || git init`
     `git lfs uninstall &>/dev/null`
     expect(Fastlane::Helper::GitHelper.is_git_repo?).to be true
     expect(Fastlane::Helper::GitHelper.has_git_lfs?).to be false
@@ -186,7 +186,7 @@
 end
 
 def setup_git_repo(dummy_file_path: nil, add_file_to_gitignore: false, commit_gitignore: false)
-  `git init --initial-branch main | git init`
+  `git init --initial-branch main || git init`
   `touch .gitignore`
   `git add .gitignore && git commit -m 'Add .gitignore'`
 

From 682acd66d9f053e27c1c5419187b11a3d0e1d1d6 Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Thu, 16 Sep 2021 22:06:54 +0200
Subject: [PATCH 09/10] Detailed YARD documentation about the various Locale
 code standards used for each key/context

+ Split locale.rb and locales.rb
---
 .../plugin/wpmreleasetoolkit/models/locale.rb | 40 +++++++++++++++++++
 .../wpmreleasetoolkit/models/locales.rb       | 16 ++------
 2 files changed, 44 insertions(+), 12 deletions(-)
 create mode 100644 lib/fastlane/plugin/wpmreleasetoolkit/models/locale.rb

diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/models/locale.rb b/lib/fastlane/plugin/wpmreleasetoolkit/models/locale.rb
new file mode 100644
index 000000000..0e37dbc62
--- /dev/null
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/models/locale.rb
@@ -0,0 +1,40 @@
+module Fastlane
+  # Defines a single Locale with the various locale codes depending on the representation needed.
+  #
+  # The various locale codes formats for the various keys can be found as follows:
+  #
+  #  - glotpress:
+  #      Go to the GP project page (e.g. https://translate.wordpress.org/projects/apps/android/dev/)
+  #      and hover over the link for each locale, locale code is in the URL.
+  #  - android: (`values-*` folder names)
+  #       See https://developer.android.com/guide/topics/resources/providing-resources#AlternativeResources (Scroll to Table 2)
+  #       [ISO639-1 (lowercase)]-r[ISO-3166-alpha-2 (uppercase)], e.g. `zh-rCN` ("Chinese understood in mainland China")
+  #  - google_play: (PlayStore Console, for metadata, release_notes.xml and `fastlane supply`)
+  #      See https://support.google.com/googleplay/android-developer/answer/9844778 (then open "View list of available languages").
+  #      See also https://github.com/fastlane/fastlane/blob/master/supply/lib/supply/languages.rb
+  #  - ios: (`*.lproj`)
+  #      See https://developer.apple.com/documentation/xcode/choosing-localization-regions-and-scripts#Understand-the-Language-Identifier
+  #      [ISO639-1/ISO639-2 (lowercase)]-[ISO 3166-1 (uppercase region or titlecase script)], e.g. `zh-Hans` ("Simplified Chinese" script)
+  #  - app_store: (AppStoreConnect, for metadata and `fastlane deliver`)
+  #      See https://github.com/fastlane/fastlane/blob/master/deliver/lib/deliver/languages.rb
+  #
+  # Links to ISO Standards
+  #   ISO standard portal: https://www.iso.org/obp/ui/#search
+  #   ISO 639-1: https://www.loc.gov/standards/iso639-2/php/code_list.php
+  #   ISO-3166-alpha2: https://www.iso.org/obp/ui/#iso:pub:PUB500001:en
+  #
+  # Notes about region vs script codes in ISO-3166-1
+  #   `zh-CN` is a locale code - Chinese understood in mainland China
+  #   `zh-Hans` is a language+script code - Chinese written in Simplified Chinese (not just understood in mainland China)
+  #
+  Locale = Struct.new(:glotpress, :android, :google_play, :ios, :app_store, keyword_init: true) do
+    # Returns the Locale with the given glotpress locale code from the list of all known locales (`Locales.all`)
+    #
+    # @param [String] The glotpress locale code for the locale to fetch
+    # @return [Locale] The locale found
+    # @raise [RuntimeException] if the locale with given glotpress code is unknown
+    def self.[](code)
+      Locales[code].first
+    end
+  end
+end
diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
index 158b9d8fb..f62a373de 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
@@ -1,19 +1,11 @@
-module Fastlane
-  # Define a single Locale with the various locale codes depending on the representation needed
-  Locale = Struct.new(:glotpress, :android, :google_play, :ios, :app_store, keyword_init: true) do
-    # Returns the Locale with the given glotpress locale code from the list of all known locales (`Locales.all`)
-    #
-    # @param [String] The glotpress locale code for the locale to fetch
-    # @return [Locale] The locale found
-    # @raise [RuntimeException] if the locale with given glotpress code is unknown
-    def self.[](code)
-      Locales[code].first
-    end
-  end
+require_relative 'locale'
 
+module Fastlane
   # A class with static methods to manipulate lists of locales.
+  #
   # Exposes various `Array<Locale>` lists like all known locales, the Mag16,
   # and convenience methods to turn list of Strings into list of Locales.
+  #
   class Locales
     ###################
     ## Constants

From 8204b59a443bebe49042d4fda3bb98ae85fb9061 Mon Sep 17 00:00:00 2001
From: Olivier Halligon <olivier.halligon@automattic.com>
Date: Thu, 16 Sep 2021 22:08:23 +0200
Subject: [PATCH 10/10] Rename from_xxx methods with a bang `!`

Per ruby conventions, as they might raise on invalid/unknown values
---
 .../plugin/wpmreleasetoolkit/models/locales.rb       |  6 +++---
 spec/locales_spec.rb                                 | 12 ++++++------
 2 files changed, 9 insertions(+), 9 deletions(-)

diff --git a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
index f62a373de..0cc6ee762 100644
--- a/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
+++ b/lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
@@ -89,7 +89,7 @@ def all
       # @raise [RuntimeException] if at least one of the locale codes was unknown
       #
       %i[glotpress android google_play ios app_store].each do |key|
-        define_method("from_#{key}") { |args| search!(key, args) }
+        define_method("from_#{key}!") { |args| search!(key, args) }
       end
 
       # Return an Array<Locale> based on glotpress locale codes
@@ -104,14 +104,14 @@ def [](*list)
         # If we passed a variadic list of Strings, `*list` will make it a single `Array<String>` and we were already good to go.
         # But if we passed an Array, `*list` will make it an Array<Array<String>> of one item; taking `list.first` will go back to Array<String>.
         list = list.first if list.count == 1 && list.first.is_a?(Array)
-        from_glotpress(list)
+        from_glotpress!(list)
       end
 
       # Return the subset of the 16 locales most of our apps are localized 100% (the ones we call the "Magnificent 16")
       #
       # @return [Array<Locale>] List of the Mag16 locales
       def mag16
-        from_glotpress(MAG16_GP_CODES)
+        from_glotpress!(MAG16_GP_CODES)
       end
 
       ###################
diff --git a/spec/locales_spec.rb b/spec/locales_spec.rb
index 6b1dd9823..269a1e9b0 100644
--- a/spec/locales_spec.rb
+++ b/spec/locales_spec.rb
@@ -2,7 +2,7 @@
 
 describe Fastlane::Locales do
   shared_examples 'from_xxx' do |key, fr_code, pt_code|
-    let(:method_sym) { "from_#{key}".to_sym }
+    let(:method_sym) { "from_#{key}!".to_sym }
 
     it 'can find a locale from a single code' do
       fr_locale = described_class.send(method_sym, fr_code)
@@ -30,23 +30,23 @@
     end
   end
 
-  describe 'from_glotpress' do
+  describe 'from_glotpress!' do
     include_examples 'from_xxx', :glotpress, 'fr', 'pt-br'
   end
 
-  describe 'from_android' do
+  describe 'from_android!' do
     include_examples 'from_xxx', :android, 'fr', 'pt-rBR'
   end
 
-  describe 'from_google_play' do
+  describe 'from_google_play!' do
     include_examples 'from_xxx', :google_play, 'fr-FR', 'pt-BR'
   end
 
-  describe 'from_ios' do
+  describe 'from_ios!' do
     include_examples 'from_xxx', :ios, 'fr-FR', 'pt-BR'
   end
 
-  describe 'from_app_store' do
+  describe 'from_app_store!' do
     include_examples 'from_xxx', :app_store, 'fr-FR', 'pt-BR'
   end