Skip to content
2 changes: 2 additions & 0 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion lib/fastlane/plugin/wpmreleasetoolkit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions lib/fastlane/plugin/wpmreleasetoolkit/models/locale.rb
Original file line number Diff line number Diff line change
@@ -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
137 changes: 137 additions & 0 deletions lib/fastlane/plugin/wpmreleasetoolkit/models/locales.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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
ALL_KNOWN_LOCALES = [
Copy link
Contributor Author

@AliSoftware AliSoftware Aug 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP: To be completed with ios: and app_store: values for each locale; we'll also need to double-check all the tuples to ensure we defined the correct ones.

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', 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'),
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', 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', 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'),
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.
#
# 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, 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) }
end

# Return an Array<Locale> based on glotpress locale codes
#
# @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 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% (the ones 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
2 changes: 1 addition & 1 deletion spec/configure_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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!)

Expand Down
14 changes: 7 additions & 7 deletions spec/git_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,41 +26,41 @@
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
end

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
end
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
Expand Down Expand Up @@ -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'`

Expand Down
20 changes: 20 additions & 0 deletions spec/locale_spec.rb
Original file line number Diff line number Diff line change
@@ -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: 'fr-FR', app_store: 'fr-FR' })
end
end
103 changes: 103 additions & 0 deletions spec/locales_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
require 'spec_helper'

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)
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 = 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)
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 do
described_class.send(method_sym, [fr_code, 'invalidcode', pt_code])
end.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

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
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 = 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)
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 = 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)
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 '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

it 'is easy to do Locale subset intersections' do
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
end