├── .ruby-version ├── .rspec ├── Gemfile ├── Rakefile ├── .rubocop.yml ├── lib ├── locale_provider.rb └── locale_map.rb ├── spec ├── spec_helper.rb ├── helpers │ └── locale_providers.rb └── locales_spec.rb ├── README.md ├── .github └── workflows │ └── build-and-test.yml ├── fastlane_locale_map.gemspec └── .gitignore /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.6.4 -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Use the depdendencies specified by the gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: rubocop-rspec 2 | 3 | Style/FrozenStringLiteralComment: 4 | Enabled: false 5 | 6 | Layout/LineLength: 7 | Max: 120 8 | 9 | Metrics/LineLength: 10 | Max: 120 -------------------------------------------------------------------------------- /lib/locale_provider.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # This module defines an interface for providing locales to a LocaleMap. 3 | module LocaleProvider 4 | def locale_map 5 | raise "You must implement `locale_map` in #{self.class}" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | require 'locale_map' 4 | require 'locale_provider' 5 | require 'helpers/locale_providers' 6 | 7 | RSpec.configure do |config| 8 | config.color = true 9 | config.tty = true 10 | config.formatter = :documentation 11 | config.filter_run_when_matching :focus 12 | end 13 | -------------------------------------------------------------------------------- /spec/helpers/locale_providers.rb: -------------------------------------------------------------------------------- 1 | class TestLocaleProvider 2 | include LocaleProvider 3 | def locale_map 4 | { 5 | arSA: { 6 | test: 'foo' 7 | }, 8 | foo: { 9 | test: 'foo' 10 | } 11 | } 12 | end 13 | end 14 | 15 | class NonConformingLocaleProvider 16 | include LocaleProvider 17 | end 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Locale Map (A Fastlane Plugin) 2 | 3 | ![CI](https://github.com/wordpress-mobile/fastlane-locale-map/workflows/CI/badge.svg?event=push) 4 | [![Coverage Status](https://coveralls.io/repos/github/wordpress-mobile/fastlane-locale-map/badge.svg?branch=trunk)](https://coveralls.io/github/wordpress-mobile/fastlane-locale-map?branch=trunk) 5 | 6 | ### A Fastlane helper plugin to wrangle locales 7 | 8 | This plugin helps you deal with differing codes for the same locales between: 9 | - The Google Play Store 10 | - App Store Connect 11 | - iOS Simulator 12 | - Android Simulator 13 | - External Localization Tooling 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ trunk ] 6 | pull_request: 7 | branches: [ trunk ] 8 | 9 | jobs: 10 | build: 11 | name: Build and Test 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Ruby 2.6 17 | uses: actions/setup-ruby@v1 18 | with: 19 | ruby-version: 2.6.x 20 | - name: Build and test with Rake 21 | env: # Or as an environment variable 22 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} 23 | CI: true 24 | run: | 25 | gem install bundler 26 | bundle install --jobs 4 --retry 3 27 | bundle exec rake 28 | -------------------------------------------------------------------------------- /fastlane_locale_map.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'fastlane_locale_map' 3 | s.version = '0.0.1' 4 | s.date = '2020-03-21' 5 | s.summary = 'Provides a list of locales for use in other Fastlane plugins' 6 | s.files = [ 7 | 'lib/locale_map.rb' 8 | ] 9 | s.require_paths = ['lib'] 10 | s.authors = ['jkmassel'] 11 | s.homepage = 'https://github.com/wordpress-mobile/fastlane-locale-map' 12 | s.license = 'MIT' 13 | 14 | s.add_dependency 'deep_merge', '~> 1.0' 15 | 16 | s.add_development_dependency 'coveralls', '~> 0.8' 17 | s.add_development_dependency 'rake', '~> 13.0' 18 | s.add_development_dependency 'rspec', '~> 3.9' 19 | s.add_development_dependency 'rubocop-rspec', '~> 1.38' 20 | end 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /spec/examples.txt 9 | /test/tmp/ 10 | /test/version_tmp/ 11 | /tmp/ 12 | 13 | # Used by dotenv library to load environment variables. 14 | # .env 15 | 16 | # Ignore Byebug command history file. 17 | .byebug_history 18 | 19 | ## Specific to RubyMotion: 20 | .dat* 21 | .repl_history 22 | build/ 23 | *.bridgesupport 24 | build-iPhoneOS/ 25 | build-iPhoneSimulator/ 26 | 27 | ## Documentation cache and generated files: 28 | /.yardoc/ 29 | /_yardoc/ 30 | /doc/ 31 | /rdoc/ 32 | 33 | ## Environment normalization: 34 | /.bundle/ 35 | /vendor/bundle 36 | /lib/bundler/man/ 37 | 38 | # for a library or gem, you might want to ignore these files since the code is 39 | # intended to run in multiple environments; otherwise, check them in: 40 | Gemfile.lock 41 | # .ruby-version 42 | # .ruby-gemset 43 | 44 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 45 | .rvmrc 46 | 47 | # Used by RuboCop. Remote config files pulled in from inherit_from directive. 48 | # .rubocop-https?--* 49 | 50 | # macOS 51 | .DS_Store 52 | -------------------------------------------------------------------------------- /spec/locales_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe LocaleMap do # rubocop:todo Metrics/BlockLength 4 | expected_locale_count = 39 5 | let(:map) { described_class.new } 6 | 7 | describe '.all' do 8 | it 'returns all locales' do 9 | expect(map.all.length).to eq expected_locale_count 10 | end 11 | 12 | it 'integrates changes to existing locales from providers' do 13 | map.add_provider TestLocaleProvider.new 14 | locales = map.all 15 | 16 | expect(locales[:arSA][:test]).to eq 'foo' 17 | end 18 | 19 | it 'integrates changes to new locales from providers' do 20 | map.add_provider TestLocaleProvider.new 21 | locales = map.all 22 | 23 | expect(locales[:foo][:test]).to eq 'foo' 24 | end 25 | end 26 | 27 | describe '.app_store_connect' do 28 | it 'returns only App Store Connect locale codes' do 29 | expect(map.app_store_connect.length).to eq expected_locale_count 30 | end 31 | 32 | it 'returns a hash' do 33 | expect(map.app_store_connect).to be_a Hash 34 | end 35 | 36 | it 'all hash values are strings' do 37 | expect(map.app_store_connect.values).to all(be_a String) 38 | end 39 | end 40 | 41 | describe '.google_play' do 42 | it 'returns only Play Store locale codes' do 43 | expect(map.google_play.length).to eq expected_locale_count 44 | end 45 | 46 | it 'returns a hash' do 47 | expect(map.google_play).to be_a Hash 48 | end 49 | 50 | it 'all hash values are strings' do 51 | expect(map.google_play.values).to all(be_a String) 52 | end 53 | end 54 | 55 | describe '.iso_codes' do 56 | it 'returns only ISO codes' do 57 | expect(map.iso_codes.length).to eq expected_locale_count 58 | end 59 | 60 | it 'returns a hash' do 61 | expect(map.iso_codes).to be_a Hash 62 | end 63 | 64 | it 'all hash values are strings' do 65 | expect(map.iso_codes.values).to all(be_a String) 66 | end 67 | 68 | it 'all languages keys are ISO locale codes with dashes removed' do 69 | map.all.each do |key, locale| 70 | expect(key.to_s).to eq locale[:iso].gsub('-', '') 71 | end 72 | end 73 | end 74 | 75 | describe '.add_provider' do 76 | it 'adds the provider to the list' do # rubocop:todo RSpec/MultipleExpectations 77 | expect(map.providers).to be_empty 78 | map.add_provider TestLocaleProvider.new 79 | expect(map.providers.length).to eq 1 80 | end 81 | 82 | it 'raises for invalid providers' do 83 | expect do 84 | map.add_provider Object.new 85 | end.to raise_error(RuntimeError) 86 | end 87 | 88 | it 'raises for non-conforming providers' do 89 | expect do 90 | map.add_provider NonConformingLocaleProvider.new 91 | end.to raise_error(RuntimeError) 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /lib/locale_map.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # This class provides a map of locale codes between Google Play and App Store Connect. 3 | # It's expected that consumers of this class will add the locale codes used by their own 4 | # systems to this mapping. 5 | # Locales are keyed by ISO code (which is included with each locale) 6 | class LocaleMap # rubocop:todo Metrics/ClassLength 7 | attr_accessor :providers 8 | 9 | def initialize 10 | @providers = [] 11 | end 12 | 13 | def all 14 | require 'deep_merge' 15 | 16 | locales = self.locales 17 | 18 | @providers.each do |provider| 19 | locales = locales.deep_merge provider.locale_map 20 | end 21 | 22 | locales 23 | end 24 | 25 | def app_store_connect 26 | only(:appStore) 27 | end 28 | 29 | def google_play 30 | only(:playStore) 31 | end 32 | 33 | def iso_codes 34 | only(:iso) 35 | end 36 | 37 | def only(token) 38 | all.each_with_object({}) do |(key, locale), hash| 39 | hash[key] = locale[token] 40 | end 41 | end 42 | 43 | def add_provider(provider) 44 | raise 'Providers must implement the `LocaleProvider` module.' unless defined? provider.locale_map 45 | raise 'Providers must return a hash object' unless provider.locale_map.is_a? Hash 46 | 47 | @providers.append provider 48 | end 49 | 50 | def remove_provider(provider) 51 | index = @providers.index(provider) 52 | @providers.delete_at index 53 | end 54 | 55 | def locales # rubocop:todo Metrics/MethodLength 56 | { 57 | arSA: { 58 | english: 'Arabic (Saudi Arabia)', 59 | iso: 'ar-SA', 60 | appStore: 'ar-SA', 61 | playStore: 'ar' 62 | }, 63 | ca: { 64 | english: 'Catalan', 65 | iso: 'ca', 66 | appStore: 'ca', 67 | playStore: 'ca' 68 | }, 69 | cs: { 70 | english: 'Czech', 71 | iso: 'cs', 72 | appStore: 'cs', 73 | playStore: 'cs' 74 | }, 75 | da: { 76 | english: 'Danish', 77 | iso: 'da', 78 | appStore: 'da', 79 | playStore: 'da-DK' 80 | }, 81 | deDE: { 82 | english: 'German (Germany)', 83 | iso: 'de-DE', 84 | appStore: 'de-DE', 85 | playStore: 'de-DE' 86 | }, 87 | el: { 88 | english: 'Greek', 89 | iso: 'el', 90 | appStore: 'el', 91 | playStore: 'el-GR' 92 | }, 93 | enAU: { 94 | english: 'English (Australia)', 95 | iso: 'en-AU', 96 | appStore: 'en-AU', 97 | playStore: 'en-AU' 98 | }, 99 | enCA: { 100 | english: 'English (Canada)', 101 | iso: 'en-CA', 102 | appStore: 'en-CA', 103 | playStore: 'en-CA' 104 | }, 105 | enGB: { 106 | english: 'English (United Kingdom)', 107 | iso: 'en-GB', 108 | appStore: 'en-GB', 109 | playStore: 'en-GB' 110 | }, 111 | enUS: { 112 | english: 'English (United States)', 113 | iso: 'en-US', 114 | appStore: 'en-US', 115 | playStore: 'en-US' 116 | }, 117 | esES: { 118 | english: 'Spanish (Spain)', 119 | iso: 'es-ES', 120 | appStore: 'es-ES', 121 | playStore: 'es-ES' 122 | }, 123 | esMX: { 124 | english: 'Spanish (Mexico)', 125 | iso: 'es-MX', 126 | appStore: 'es-MX', 127 | playStore: 'es-MX' 128 | }, 129 | fi: { 130 | english: 'Finnish', 131 | iso: 'fi', 132 | appStore: 'fi', 133 | playStore: 'fi-FI' 134 | }, 135 | frCA: { 136 | english: 'French (Canada)', 137 | iso: 'fr-CA', 138 | appStore: 'fr-CA', 139 | playStore: 'fr-CA' 140 | }, 141 | frFR: { 142 | english: 'French (France)', 143 | iso: 'fr-FR', 144 | appStore: 'fr-FR', 145 | playStore: 'fr-FR' 146 | }, 147 | he: { 148 | english: 'Hebrew', 149 | iso: 'he', 150 | appStore: 'he', 151 | playStore: 'iw-IL' 152 | }, 153 | hi: { 154 | english: 'Hindi', 155 | iso: 'hi', 156 | appStore: 'hi', 157 | playStore: 'hi-IN' 158 | }, 159 | hr: { 160 | english: 'Croatian', 161 | iso: 'hr', 162 | appStore: 'hr', 163 | playStore: 'hr' 164 | }, 165 | hu: { 166 | english: 'Hungarian', 167 | iso: 'hu', 168 | appStore: 'hu', 169 | playStore: 'hu-HU' 170 | }, 171 | id: { 172 | english: 'Indonesian', 173 | iso: 'id', 174 | appStore: 'id', 175 | playStore: 'id' 176 | }, 177 | it: { 178 | english: 'Italian', 179 | iso: 'it', 180 | appStore: 'it', 181 | playStore: 'it-IT' 182 | }, 183 | ja: { 184 | english: 'Japanese', 185 | iso: 'ja', 186 | appStore: 'ja', 187 | playStore: 'ja-JP' 188 | }, 189 | ko: { 190 | english: 'Korean', 191 | iso: 'ko', 192 | appStore: 'ko', 193 | playStore: 'ko-KR' 194 | }, 195 | ms: { 196 | english: 'Malay', 197 | iso: 'ms', 198 | appStore: 'ms', 199 | playStore: 'ms' 200 | }, 201 | nlNL: { 202 | english: 'Dutch (Netherlands)', 203 | iso: 'nl-NL', 204 | appStore: 'nl-NL', 205 | playStore: 'nl-NL' 206 | }, 207 | nb: { 208 | english: 'Norwegian', 209 | iso: 'nb', 210 | appStore: 'no', 211 | playStore: 'no-NO' 212 | }, 213 | pl: { 214 | english: 'Polish', 215 | iso: 'pl', 216 | appStore: 'pl', 217 | playStore: 'pl-PL' 218 | }, 219 | ptBR: { 220 | english: 'Portuguese (Brazil)', 221 | iso: 'pt-BR', 222 | appStore: 'pt-BR', 223 | playStore: 'pt-BR' 224 | }, 225 | ptPT: { 226 | english: 'Portuguese (Portugal)', 227 | iso: 'pt-PT', 228 | appStore: 'pt-PT', 229 | playStore: 'pt-PT' 230 | }, 231 | ro: { 232 | english: 'Romanian', 233 | iso: 'ro', 234 | appStore: 'ro', 235 | playStore: 'ro' 236 | }, 237 | ru: { 238 | english: 'Russian', 239 | iso: 'ru', 240 | appStore: 'ru', 241 | playStore: 'ru-RU' 242 | }, 243 | sk: { 244 | english: 'Slovak', 245 | iso: 'sk', 246 | appStore: 'sk', 247 | playStore: 'sk' 248 | }, 249 | sv: { 250 | english: 'Swedish', 251 | iso: 'sv', 252 | appStore: 'sv', 253 | playStore: 'sv-SE' 254 | }, 255 | th: { 256 | english: 'Thai', 257 | iso: 'th', 258 | appStore: 'th', 259 | playStore: 'th' 260 | }, 261 | tr: { 262 | english: 'Turkish', 263 | iso: 'tr', 264 | appStore: 'tr', 265 | playStore: 'tr-TR' 266 | }, 267 | uk: { 268 | english: 'Ukrainian', 269 | iso: 'uk', 270 | appStore: 'uk', 271 | playStore: 'uk' 272 | }, 273 | vi: { 274 | english: 'Vietnamese', 275 | iso: 'vi', 276 | appStore: 'vi', 277 | playStore: 'vi' 278 | }, 279 | zhCN: { 280 | english: 'Chinese (Simplified)', 281 | iso: 'zh-CN', 282 | appStore: 'zh-Hans', 283 | playStore: 'zh-CN' 284 | }, 285 | zhTW: { 286 | english: 'Chinese (Traditional)', 287 | iso: 'zh-TW', 288 | appStore: 'zh-Hant', 289 | playStore: 'zh-TW' 290 | } 291 | } 292 | end 293 | end 294 | --------------------------------------------------------------------------------