rails_translation_manager 0.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -1
- data/.ruby-version +1 -1
- data/CHANGELOG.md +20 -0
- data/Jenkinsfile +3 -1
- data/Rakefile +13 -7
- data/config/locales/plurals.rb +49 -0
- data/lib/rails_translation_manager/cleaner.rb +14 -0
- data/lib/rails_translation_manager/i18n_tasks_option_parser.rb +10 -0
- data/lib/rails_translation_manager/importer.rb +32 -14
- data/lib/rails_translation_manager/locale_checker/all_locales.rb +61 -0
- data/lib/rails_translation_manager/locale_checker/base_checker.rb +13 -0
- data/lib/rails_translation_manager/locale_checker/incompatible_plurals.rb +79 -0
- data/lib/rails_translation_manager/locale_checker/locale_checker_helper.rb +23 -0
- data/lib/rails_translation_manager/locale_checker/missing_english_locales.rb +31 -0
- data/lib/rails_translation_manager/locale_checker/missing_foreign_locales.rb +31 -0
- data/lib/rails_translation_manager/locale_checker/plural_forms.rb +16 -0
- data/lib/rails_translation_manager/locale_checker.rb +50 -0
- data/lib/rails_translation_manager/version.rb +1 -1
- data/lib/rails_translation_manager.rb +21 -3
- data/lib/tasks/translation.rake +43 -42
- data/rails_translation_manager.gemspec +6 -2
- data/spec/locales/cleaner/clean.yml +5 -0
- data/spec/locales/cleaner/with_whitespace.yml +5 -0
- data/spec/locales/importer/fr.csv +8 -0
- data/spec/locales/importer/hy_with_byte_order_mark.csv +4 -0
- data/spec/locales/in_sync/cy/browse.yml +12 -0
- data/spec/locales/in_sync/en/browse.yml +8 -0
- data/spec/locales/out_of_sync/cy.yml +3 -0
- data/spec/locales/out_of_sync/en.yml +6 -0
- data/spec/rails_translation_manager/cleaner_spec.rb +13 -0
- data/spec/rails_translation_manager/importer_spec.rb +93 -0
- data/spec/rails_translation_manager/locale_checker/all_locales_spec.rb +24 -0
- data/spec/rails_translation_manager/locale_checker/incompatible_plurals_spec.rb +94 -0
- data/spec/rails_translation_manager/locale_checker/locale_checker_helper_spec.rb +61 -0
- data/spec/rails_translation_manager/locale_checker/missing_english_locales_spec.rb +72 -0
- data/spec/rails_translation_manager/locale_checker/missing_foreign_locales_spec.rb +80 -0
- data/spec/rails_translation_manager/locale_checker/plural_forms_spec.rb +27 -0
- data/spec/rails_translation_manager/locale_checker_spec.rb +68 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/support/tasks.rb +7 -0
- data/spec/tasks/translation_spec.rb +127 -0
- data/test/rails_translation_manager/yaml_writer_test.rb +2 -0
- data/tmp/.gitkeep +0 -0
- metadata +117 -18
- data/lib/rails_translation_manager/stealer.rb +0 -85
- data/lib/rails_translation_manager/validator.rb +0 -92
- data/test/rails_translation_manager/importer_test.rb +0 -132
- data/test/rails_translation_manager/stealer_test.rb +0 -251
- data/test/rails_translation_manager/validator_test.rb +0 -51
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 6e7ef3e878a1b5ffc7ae48ad8f683887e79073b3f34a5a3439656f5f7abb6e20
         | 
| 4 | 
            +
              data.tar.gz: 2aac75f9cae7275812da73da0973de515baf0d78eb63e64182e683f8717696ca
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: f0b2fa6abaadb21ea912976b1092c6459ea0194a8e19092c522e38224da218395d230936d0d4a0aa830e3d37bd9f6eb6ac1e4909bd5406369c0aa99d1a4d13f4
         | 
| 7 | 
            +
              data.tar.gz: 0dbbefe8208052b63f5ddc80734340ed7985b199f9113aaf5457c48d50e34e889e7379a20c61963b002343f4fb0d35aacc0307b442a15a2b7fbdea77c25349dd
         | 
    
        data/.gitignore
    CHANGED
    
    
    
        data/.ruby-version
    CHANGED
    
    | @@ -1 +1 @@ | |
| 1 | 
            -
            2. | 
| 1 | 
            +
            2.6.6
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,3 +1,23 @@ | |
| 1 | 
            +
            ## 1.1.2
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Handle importing files that contain Byte Order Marks. https://github.com/alphagov/rails_translation_manager/pull/27
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ## 1.1.1
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Fix Rails Translation Manager / Rails naming clash for class. https://github.com/alphagov/rails_translation_manager/pull/26
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            ## 1.1.0
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            Allow multiple files per language to be imported. https://github.com/alphagov/rails_translation_manager/pull/20
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            ## 1.0.0
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            Adds logic to verify locale files are in sync with each other and have the
         | 
| 16 | 
            +
            correct plural forms. Also introduces RSpec into the Gem which will replace
         | 
| 17 | 
            +
            Minitest in the coming iterations.
         | 
| 18 | 
            +
             | 
| 19 | 
            +
            Integrates RTM with I18n-tasks. Adds wrappers around `add-missing` & `normalize` tasks, adds Cleaner class to remove the whitespace added by i18n-tasks, adds tests and byebug gem as a debugging tool.
         | 
| 20 | 
            +
             | 
| 1 21 | 
             
            ## 0.1.0
         | 
| 2 22 |  | 
| 3 23 | 
             
            Don't change the $LOAD_PATH in translation.rake.
         | 
    
        data/Jenkinsfile
    CHANGED
    
    
    
        data/Rakefile
    CHANGED
    
    | @@ -1,11 +1,17 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
            require "rake/testtask"
         | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 3 2 |  | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 3 | 
            +
            require 'rspec/core/rake_task'
         | 
| 4 | 
            +
            require 'bundler/gem_tasks'
         | 
| 5 | 
            +
            require 'rake/testtask'
         | 
| 6 | 
            +
            require 'rails_i18n'
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            Rake::TestTask.new('test') do |t|
         | 
| 9 | 
            +
              t.description = 'Run tests'
         | 
| 10 | 
            +
              t.libs << 'test'
         | 
| 11 | 
            +
              t.test_files = FileList['test/**/*_test.rb']
         | 
| 8 12 | 
             
              t.verbose = true
         | 
| 9 13 | 
             
            end
         | 
| 10 14 |  | 
| 11 | 
            -
             | 
| 15 | 
            +
            RSpec::Core::RakeTask.new(:spec)
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            task default: %i[spec test]
         | 
| @@ -0,0 +1,49 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            {
         | 
| 4 | 
            +
              # Dari - this isn't an iso code. Probably should be 'prs' as per ISO 639-3.
         | 
| 5 | 
            +
              dr: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 6 | 
            +
              # Latin America and Caribbean Spanish
         | 
| 7 | 
            +
              "es-419": { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 8 | 
            +
              # Scottish Gaelic
         | 
| 9 | 
            +
              gd: { i18n: { plural: { keys: %i[one two few other],
         | 
| 10 | 
            +
                                      rule: lambda do |n|
         | 
| 11 | 
            +
                                        if [1, 11].include?(n)
         | 
| 12 | 
            +
                                          :one
         | 
| 13 | 
            +
                                        elsif [2, 12].include?(n)
         | 
| 14 | 
            +
                                          :two
         | 
| 15 | 
            +
                                        elsif [3, 4, 5, 6, 7, 8, 9, 10, 13, 14, 15, 16, 17, 18, 19].include?(n)
         | 
| 16 | 
            +
                                          :few
         | 
| 17 | 
            +
                                        else
         | 
| 18 | 
            +
                                          :other
         | 
| 19 | 
            +
                                        end
         | 
| 20 | 
            +
                                      end } } },
         | 
| 21 | 
            +
              # Gujarati
         | 
| 22 | 
            +
              gu: { i18n: { plural: { keys: %i[one other], rule: ->(n) { [0, 1].include?(n) ? :one : :other } } } },
         | 
| 23 | 
            +
              # Armenian
         | 
| 24 | 
            +
              hy: { i18n: { plural: { keys: %i[one other], rule: ->(n) { [0, 1].include?(n) ? :one : :other } } } },
         | 
| 25 | 
            +
              # Kazakh
         | 
| 26 | 
            +
              kk: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 27 | 
            +
              # Pashto
         | 
| 28 | 
            +
              ps: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 29 | 
            +
              # Punjabi Shahmukhi
         | 
| 30 | 
            +
              "pa-pk": { i18n: { plural: { keys: %i[one other], rule: ->(n) { [0, 1].include?(n) ? :one : :other } } } },
         | 
| 31 | 
            +
              # Sinhalese
         | 
| 32 | 
            +
              si: { i18n: { plural: { keys: %i[one other], rule: ->(n) { [0, 1].include?(n) ? :one : :other } } } },
         | 
| 33 | 
            +
              # Somali
         | 
| 34 | 
            +
              so: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 35 | 
            +
              # Albanian
         | 
| 36 | 
            +
              sq: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 37 | 
            +
              # Norwegian
         | 
| 38 | 
            +
              no: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 39 | 
            +
              # Tamil
         | 
| 40 | 
            +
              ta: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 41 | 
            +
              # Turkmen
         | 
| 42 | 
            +
              tk: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 43 | 
            +
              # Uzbek
         | 
| 44 | 
            +
              uz: { i18n: { plural: { keys: %i[one other], rule: ->(n) { n == 1 ? :one : :other } } } },
         | 
| 45 | 
            +
              # Chinese Hong Kong
         | 
| 46 | 
            +
              'zh-hk' => { i18n: { plural: { keys: %i[other], rule: -> { :other } } } },
         | 
| 47 | 
            +
              # Chinese Taiwan
         | 
| 48 | 
            +
              'zh-tw' => { i18n: { plural: { keys: %i[other], rule: -> { :other } } } }
         | 
| 49 | 
            +
            }
         | 
| @@ -5,36 +5,39 @@ require_relative "yaml_writer" | |
| 5 5 | 
             
            class RailsTranslationManager::Importer
         | 
| 6 6 | 
             
              include YAMLWriter
         | 
| 7 7 |  | 
| 8 | 
            -
               | 
| 9 | 
            -
             | 
| 8 | 
            +
              attr_reader :locale, :csv_path, :import_directory, :multiple_files_per_language
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def initialize(locale:, csv_path:, import_directory:, multiple_files_per_language:)
         | 
| 10 11 | 
             
                @locale = locale
         | 
| 12 | 
            +
                @csv_path = csv_path
         | 
| 11 13 | 
             
                @import_directory = import_directory
         | 
| 14 | 
            +
                @multiple_files_per_language = multiple_files_per_language
         | 
| 12 15 | 
             
              end
         | 
| 13 16 |  | 
| 14 17 | 
             
              def import
         | 
| 15 | 
            -
                csv = CSV.read( | 
| 16 | 
            -
             | 
| 17 | 
            -
                csv | 
| 18 | 
            +
                csv = CSV.read(csv_path, encoding: "bom|utf-8", headers: true, header_converters: :downcase)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                multiple_files_per_language ? import_csv_into_multiple_files(csv) : import_csv(csv)
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              private
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              def import_csv(csv, import_yml_path = File.join(import_directory, "#{locale}.yml"))
         | 
| 26 | 
            +
                data = csv.each_with_object({}) do |row, hash|
         | 
| 18 27 | 
             
                  key = row["key"]
         | 
| 19 28 | 
             
                  key_parts = key.split(".")
         | 
| 20 29 | 
             
                  if key_parts.length > 1
         | 
| 21 | 
            -
                    leaf_node = ( | 
| 30 | 
            +
                    leaf_node = (hash[key_parts.first] ||= {})
         | 
| 22 31 | 
             
                    key_parts[1..-2].each do |part|
         | 
| 23 32 | 
             
                      leaf_node = (leaf_node[part] ||= {})
         | 
| 24 33 | 
             
                    end
         | 
| 25 34 | 
             
                    leaf_node[key_parts.last] = parse_translation(row["translation"])
         | 
| 26 35 | 
             
                  else
         | 
| 27 | 
            -
                     | 
| 36 | 
            +
                    hash[key_parts.first] = parse_translation(row["translation"])
         | 
| 28 37 | 
             
                  end
         | 
| 29 38 | 
             
                end
         | 
| 30 39 |  | 
| 31 | 
            -
                write_yaml(import_yml_path, { | 
| 32 | 
            -
              end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
              private
         | 
| 35 | 
            -
             | 
| 36 | 
            -
              def import_yml_path
         | 
| 37 | 
            -
                File.join(@import_directory, "#{@locale}.yml")
         | 
| 40 | 
            +
                write_yaml(import_yml_path, { locale.to_s => data })
         | 
| 38 41 | 
             
              end
         | 
| 39 42 |  | 
| 40 43 | 
             
              def parse_translation(translation)
         | 
| @@ -55,4 +58,19 @@ class RailsTranslationManager::Importer | |
| 55 58 | 
             
                  translation
         | 
| 56 59 | 
             
                end
         | 
| 57 60 | 
             
              end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
              def import_csv_into_multiple_files(csv)
         | 
| 63 | 
            +
                group_csv_by_file(csv).each do |group|
         | 
| 64 | 
            +
                  language_dir =  File.join(import_directory, locale)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  Dir.mkdir(language_dir) unless Dir.exists?(language_dir)
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  import_yml_path = File.join(import_directory, locale, "#{group[0]}.yml")
         | 
| 69 | 
            +
                  import_csv(group[1], import_yml_path)
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              def group_csv_by_file(csv)
         | 
| 74 | 
            +
                csv.group_by { |row| row["key"].split(".").first }
         | 
| 75 | 
            +
              end
         | 
| 58 76 | 
             
            end
         | 
| @@ -0,0 +1,61 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class AllLocales
         | 
| 4 | 
            +
              attr_reader :locale_path
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def initialize(locale_path)
         | 
| 7 | 
            +
                @locale_path = locale_path
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def generate
         | 
| 11 | 
            +
                paths = locale_file_paths.compact
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                raise NoLocaleFilesFound, "No locale files found for the supplied path" if paths.blank?
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                paths.flat_map do |locale_group|
         | 
| 16 | 
            +
                  {
         | 
| 17 | 
            +
                    locale: locale_group[:locale],
         | 
| 18 | 
            +
                    keys: all_keys_for_locale(locale_group)
         | 
| 19 | 
            +
                  }
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              private
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              def locale_file_paths
         | 
| 26 | 
            +
                I18n.available_locales.map do |locale|
         | 
| 27 | 
            +
                  grouped_paths = Dir[locale_path].select do |path|
         | 
| 28 | 
            +
                    locale = locale.downcase
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    path =~ %r{/#{locale}/} || path =~ %r{/#{locale}\.yml$}
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                  { locale: locale.downcase, paths: grouped_paths } if grouped_paths.present?
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              def all_keys_for_locale(locale_group)
         | 
| 38 | 
            +
                locale_group[:paths].flat_map do |path|
         | 
| 39 | 
            +
                  keys_from_file(file_path: path)
         | 
| 40 | 
            +
                end
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def keys_from_file(locale_hash: nil, key_chain: nil, locale_keys: [], file_path: nil)
         | 
| 44 | 
            +
                locale_hash ||= YAML.load_file(file_path)
         | 
| 45 | 
            +
                keys = locale_hash.keys
         | 
| 46 | 
            +
                keys.each do |key|
         | 
| 47 | 
            +
                  if locale_hash.fetch(key).is_a?(Hash)
         | 
| 48 | 
            +
                    keys_from_file(locale_hash: locale_hash.fetch(key), key_chain: "#{key_chain}.#{key}", locale_keys: locale_keys)
         | 
| 49 | 
            +
                  else
         | 
| 50 | 
            +
                    keys.each do |final_key|
         | 
| 51 | 
            +
                      locale_keys << "#{key_chain}.#{final_key}"
         | 
| 52 | 
            +
                    end
         | 
| 53 | 
            +
                  end
         | 
| 54 | 
            +
                end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                # remove locale prefix from keys, e.g: ".en.browse.page" -> "browse.page"
         | 
| 57 | 
            +
                locale_keys.uniq.map { |key| key.split(".")[2..].join(".") }
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              class NoLocaleFilesFound < StandardError; end
         | 
| 61 | 
            +
            end
         | 
| @@ -0,0 +1,79 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class IncompatiblePlurals < BaseChecker
         | 
| 4 | 
            +
              include LocaleCheckerHelper
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def report
         | 
| 7 | 
            +
                format_plural_errors if plural_errors.present?
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              private
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              def format_plural_errors
         | 
| 13 | 
            +
                <<~OUTPUT.chomp
         | 
| 14 | 
            +
                  \e[31m[ERROR]\e[0m Incompatible plural forms, for:
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  #{plural_errors.join("\n\n")}
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  \e[1mIf the keys reported above are not plurals, rename them avoiding plural keywords: #{PLURAL_KEYS}\e[22m
         | 
| 19 | 
            +
                OUTPUT
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              def plural_errors
         | 
| 23 | 
            +
                @plural_errors ||= grouped_plural_keys_for_locale.flat_map do |plurals|
         | 
| 24 | 
            +
                  plural_form = all_plural_forms[plurals[:locale]]
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  if plural_form.blank?
         | 
| 27 | 
            +
                    "- \e[31m[ERROR]\e[0m Please add plural form for '#{plurals[:locale]}' <link to future documentation>"
         | 
| 28 | 
            +
                  else
         | 
| 29 | 
            +
                    incompatible_plurals(plurals, plural_form)
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              def incompatible_plurals(plurals, plural_form)
         | 
| 35 | 
            +
                error_messages = plurals[:groups].map do |plural_group|
         | 
| 36 | 
            +
                  actual_plural_form = plural_group[:keys].sort
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  next if actual_plural_form == plural_form.sort
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  "- '#{plurals[:locale]}', with parent '#{plural_group[:parent]}'. Expected: #{plural_form}, actual: #{actual_plural_form}"
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                error_messages.compact
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              def grouped_plural_keys_for_locale
         | 
| 47 | 
            +
                all_locales.map do |locale|
         | 
| 48 | 
            +
                  {
         | 
| 49 | 
            +
                    locale: locale[:locale],
         | 
| 50 | 
            +
                    groups: grouped_plural_keys(only_plurals(locale[:keys]))
         | 
| 51 | 
            +
                  }
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              def grouped_plural_keys(keys)
         | 
| 56 | 
            +
                # %w[parent.key parent.other_key] -> [{ parent: "parent", keys: %w[key other_key] }]
         | 
| 57 | 
            +
                group_by_parent_keys(keys).map do |plural_key_group|
         | 
| 58 | 
            +
                  {
         | 
| 59 | 
            +
                    parent: plural_key_group.first,
         | 
| 60 | 
            +
                    keys: plural_key_group.last.map { |key_chain| key_chain.split(".").last.to_sym }
         | 
| 61 | 
            +
                  }
         | 
| 62 | 
            +
                end
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
              def group_by_parent_keys(keys)
         | 
| 66 | 
            +
                # %w[parent.key parent.other_key] -> { "parent" => %w[parent.key parent.other_key] }
         | 
| 67 | 
            +
                keys.group_by do |key|
         | 
| 68 | 
            +
                  key.split('.')[0..-2].join('.')
         | 
| 69 | 
            +
                end
         | 
| 70 | 
            +
              end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
              def only_plurals(keys)
         | 
| 73 | 
            +
                keys.select { |key| key_is_plural?(key) }
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
              def all_plural_forms
         | 
| 77 | 
            +
                @all_plural_forms ||= PluralForms.all
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module LocaleCheckerHelper
         | 
| 4 | 
            +
              PLURAL_KEYS = %w[zero one two few many other].freeze
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def exclude_plurals(keys)
         | 
| 7 | 
            +
                keys.reject { |key| key_is_plural?(key) }
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def group_keys(locales)
         | 
| 11 | 
            +
                locales.keys.group_by do |key|
         | 
| 12 | 
            +
                  locales[key]
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              def english_keys_excluding_plurals(all_locales)
         | 
| 17 | 
            +
                exclude_plurals(all_locales.find { |locale| locale[:locale] == :en }[:keys])
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              def key_is_plural?(key)
         | 
| 21 | 
            +
                PLURAL_KEYS.include?(key.split(".").last)
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class MissingEnglishLocales < BaseChecker
         | 
| 4 | 
            +
              include LocaleCheckerHelper
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def report
         | 
| 7 | 
            +
                grouped_missing_keys = group_keys(missing_english_locales)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                format_missing_english_locales(grouped_missing_keys) if grouped_missing_keys.present?
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              private
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def format_missing_english_locales(keys)
         | 
| 15 | 
            +
                formatted_keys = keys.to_a.map do |group|
         | 
| 16 | 
            +
                  "\e[1mMissing English keys:\e[22m #{group[0]}\n\e[1mFound in:\e[22m #{group[1]}"
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                "\e[31m[ERROR]\e[0m Missing English locales, either remove these keys from the foreign locales or add them to the English locales\n\n#{formatted_keys.join("\n\n")}"
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              def missing_english_locales
         | 
| 23 | 
            +
                all_locales.each_with_object({}) do |locale, hsh|
         | 
| 24 | 
            +
                  missing_keys = exclude_plurals(locale[:keys]) - english_keys_excluding_plurals(all_locales)
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  next if missing_keys.blank?
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  hsh[locale[:locale]] = missing_keys
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class MissingForeignLocales < BaseChecker
         | 
| 4 | 
            +
              include LocaleCheckerHelper
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def report
         | 
| 7 | 
            +
                grouped_missing_keys = group_keys(missing_foreign_locales)
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                format_missing_foreign_locales(grouped_missing_keys) if grouped_missing_keys.present?
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              private
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              def format_missing_foreign_locales(keys)
         | 
| 15 | 
            +
                formatted_keys = keys.to_a.map do |group|
         | 
| 16 | 
            +
                  "\e[1mMissing foreign keys:\e[22m #{group[0]}\n\e[1mAbsent from:\e[22m #{group[1]}"
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                "\e[31m[ERROR]\e[0m Missing foreign locales, either add these keys to the foreign locales or remove them from the English locales\e[0m\n\n#{formatted_keys.join("\n\n")}"
         | 
| 20 | 
            +
              end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
              def missing_foreign_locales
         | 
| 23 | 
            +
                all_locales.each_with_object({}) do |locale, hsh|
         | 
| 24 | 
            +
                  missing_keys = english_keys_excluding_plurals(all_locales) - exclude_plurals(locale[:keys])
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  next if missing_keys.blank?
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  hsh[locale[:locale]] = missing_keys
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| @@ -0,0 +1,16 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class PluralForms
         | 
| 4 | 
            +
              def self.all
         | 
| 5 | 
            +
                I18n.available_locales.each_with_object({}) do |locale, hsh|
         | 
| 6 | 
            +
                  begin
         | 
| 7 | 
            +
                    # fetches plural form (rule) from rails-i18n for locale
         | 
| 8 | 
            +
                    plural_form = I18n.with_locale(locale) { I18n.t!("i18n.plural.keys") }.sort
         | 
| 9 | 
            +
                  rescue I18n::MissingTranslationData
         | 
| 10 | 
            +
                    plural_form = nil
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  hsh[locale.downcase] = plural_form
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
            end
         | 
| @@ -0,0 +1,50 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module RailsTranslationManager
         | 
| 4 | 
            +
              class LocaleChecker
         | 
| 5 | 
            +
                attr_reader :locale_path
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                CHECKER_CLASSES = [MissingEnglishLocales,
         | 
| 8 | 
            +
                                   MissingForeignLocales,
         | 
| 9 | 
            +
                                   IncompatiblePlurals].freeze
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                def initialize(locale_path)
         | 
| 12 | 
            +
                  @locale_path = locale_path
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def validate_locales
         | 
| 16 | 
            +
                  output_result
         | 
| 17 | 
            +
                rescue AllLocales::NoLocaleFilesFound => e
         | 
| 18 | 
            +
                  puts e
         | 
| 19 | 
            +
                  false
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                private
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def output_result
         | 
| 25 | 
            +
                  errors = checker_errors.compact
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  if errors.blank?
         | 
| 28 | 
            +
                    puts "Locale files are in sync, nice job!"
         | 
| 29 | 
            +
                    true
         | 
| 30 | 
            +
                  else
         | 
| 31 | 
            +
                    errors.each do |error_message|
         | 
| 32 | 
            +
                      puts "\n"
         | 
| 33 | 
            +
                      puts error_message
         | 
| 34 | 
            +
                      puts "\n"
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                    false
         | 
| 37 | 
            +
                  end
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                def checker_errors
         | 
| 41 | 
            +
                  CHECKER_CLASSES.flat_map do |checker|
         | 
| 42 | 
            +
                    checker.new(all_locales).report
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                def all_locales
         | 
| 47 | 
            +
                  @all_locales ||= AllLocales.new(locale_path).generate
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
            end
         | 
| @@ -1,9 +1,27 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            require "rails_translation_manager/version"
         | 
| 2 4 | 
             
            require "rails_translation_manager/railtie" if defined?(Rails)
         | 
| 3 5 | 
             
            require "rails-i18n"
         | 
| 4 6 |  | 
| 7 | 
            +
            require "rails_translation_manager/locale_checker/base_checker"
         | 
| 8 | 
            +
            require "rails_translation_manager/locale_checker/locale_checker_helper"
         | 
| 9 | 
            +
            require "rails_translation_manager/locale_checker/missing_foreign_locales"
         | 
| 10 | 
            +
            require "rails_translation_manager/locale_checker/missing_english_locales"
         | 
| 11 | 
            +
            require "rails_translation_manager/locale_checker/plural_forms"
         | 
| 12 | 
            +
            require "rails_translation_manager/locale_checker/incompatible_plurals"
         | 
| 13 | 
            +
            require "rails_translation_manager/locale_checker/all_locales"
         | 
| 14 | 
            +
            require "rails_translation_manager/locale_checker"
         | 
| 15 | 
            +
            require "rails_translation_manager/cleaner"
         | 
| 16 | 
            +
            require "rails_translation_manager/exporter"
         | 
| 17 | 
            +
            require "rails_translation_manager/importer"
         | 
| 18 | 
            +
             | 
| 5 19 | 
             
            module RailsTranslationManager
         | 
| 6 | 
            -
               | 
| 7 | 
            -
               | 
| 8 | 
            -
             | 
| 20 | 
            +
              rails_i18n_path = Gem::Specification.find_by_name("rails-i18n").gem_dir
         | 
| 21 | 
            +
              rails_translation_manager = Gem::Specification.find_by_name("rails_translation_manager").gem_dir
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              I18n.load_path.concat(
         | 
| 24 | 
            +
                Dir["#{rails_i18n_path}/rails/pluralization/*.rb"],
         | 
| 25 | 
            +
                ["#{rails_translation_manager}/config/locales/plurals.rb"]
         | 
| 26 | 
            +
              )
         | 
| 9 27 | 
             
            end
         |