class Lit::Import

Attributes

format[R]
input[R]
locale_keys[R]
skip_nil[R]

Public Class Methods

call(**kwargs) click to toggle source
# File lib/lit/import.rb, line 6
def call(**kwargs)
  new(**kwargs).perform
end
new(input:, format:, locale_keys: [], skip_nil: true, dry_run: false, raw: false) click to toggle source
# File lib/lit/import.rb, line 13
def initialize(input:, format:, locale_keys: [], skip_nil: true, dry_run: false, raw: false)
  raise ArgumentError, 'format must be yaml or csv' if %i[yaml csv].exclude?(format.to_sym)
  @input = input
  @locale_keys = locale_keys.presence || I18n.available_locales
  @format = format
  @skip_nil = skip_nil
  @dry_run = dry_run
  @raw = raw
end

Public Instance Methods

perform() click to toggle source
# File lib/lit/import.rb, line 23
def perform
  send(:"import_#{format}")
end

Private Instance Methods

concatenate_arrays(csv) click to toggle source
# File lib/lit/import.rb, line 157
def concatenate_arrays(csv) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/LineLength
  csv.inject([]) do |accu, row|
    if row.first == accu.last&.first # equal keys
      accu.tap do
        accu[-1] = [
          row.first,
          *accu[-1].drop(1)
                   .map { |x| Array.wrap(x).presence || [x] }
                   .zip(row.drop(1)).map(&:flatten)
        ]
      end
    else
      accu << row
    end
  end
end
import_csv() click to toggle source
# File lib/lit/import.rb, line 44
def import_csv
  validate_csv
  processed_csv = preprocess_csv

  processed_csv.each do |row|
    key = row.first
    row_translations = Hash[locales_in_csv.zip(row.drop(1))]
    row_translations.each do |locale, value|
      next unless locale_keys.blank? || locale_keys.map(&:to_sym).include?(locale.to_sym)
      next if value.nil? && skip_nil
      upsert(locale, key, value)
    end
  end
rescue CSV::MalformedCSVError => e
  raise ArgumentError, "Invalid CSV file: #{e.message}", cause: e
end
import_yaml() click to toggle source
# File lib/lit/import.rb, line 29
def import_yaml
  validate_yaml
  locale_keys.each do |locale|
    I18n.with_locale(locale) do
      yml = parsed_yaml[locale.to_s]
      Hash[*Lit::Cache.flatten_hash(yml)].each do |key, default_translation|
        next if default_translation.nil? && skip_nil
        upsert(locale, key, default_translation)
      end
    end
  end
rescue Psych::SyntaxError => e
  raise ArgumentError, "Invalid YAML file: #{e.message}", cause: e
end
locales_in_csv() click to toggle source
# File lib/lit/import.rb, line 115
def locales_in_csv
  @locales_in_csv ||= parsed_csv.first.drop(1)
end
parsed_csv() click to toggle source
# File lib/lit/import.rb, line 100
def parsed_csv
  @parsed_csv ||=
    begin
      CSV.parse(input)
    rescue CSV::MalformedCSVError
      # Some Excel versions tend to save CSVs with columns separated with tabs instead
      # of commas. Let's try that out if needed.
      CSV.parse(input, col_sep: "\t")
    end
end
parsed_yaml() click to toggle source
# File lib/lit/import.rb, line 111
def parsed_yaml
  @parsed_yaml ||= YAML.load(input)
end
preprocess_csv() click to toggle source

the main task of this routine is to replace blanks with nils (in CSV it cannot be distinguished, so in order for :skip_nil option to work as intended blanks must be treated as nil); as well as that, we need to look for multiple occurrences of certain keys and merge them into arrays

# File lib/lit/import.rb, line 96
def preprocess_csv
  concatenate_arrays(replace_blanks(parsed_csv))
end
replace_blanks(csv) click to toggle source
# File lib/lit/import.rb, line 174
def replace_blanks(csv)
  csv.drop(1).each do |row|
    row.replace(row.map(&:presence))
  end
end
upsert(locale, key, value) click to toggle source

This is mean to insert a value for a key in a given locale using some kind of strategy which depends on the service's options.

For instance, when @raw option is true (it's the default), if a key already exists, it overrides the default_value of the existing localization key; otherwise, with @raw set to false, it keeps the default as it is and, no matter if a translated value is there, translated_value is overridden with the imported one and is_changed is set to true.

# File lib/lit/import.rb, line 128
def upsert(locale, key, value) # rubocop:disable Metrics/MethodLength
  I18n.with_locale(locale) do
    # when an array has to be inserted with a default value, it needs to
    # be done like:
    # I18n.t('foo', default: [['bar', 'baz']])
    # because without the double array, array items are treated as fallback keys
    # - then, the last array element is the final fallback; so in this case we
    # don't specify fallback keys and only specify the final fallback, which
    # is the array
    val = value.is_a?(Array) ? [value] : value
    I18n.t(key, default: val)

    # this indicates that this translation already exists
    existing_translation =
    Lit::Localization.joins(:locale, :localization_key)
                     .find_by('localization_key = ? and locale = ?', key, locale)

    return unless existing_translation

    if @raw
      existing_translation.update(default_value: value)
    else
      existing_translation.update(translated_value: value, is_changed: true)
      lkey = existing_translation.localization_key
      lkey.update(is_deleted: false) if lkey.is_deleted
    end
  end
end
validate_csv() click to toggle source
# File lib/lit/import.rb, line 75
def validate_csv # rubocop:disable Metrics/AbcSize
  errors = []

  # CSV may not be empty
  errors << :csv_is_empty if parsed_csv.empty?

  # verify CSV header
  if !parsed_csv.empty? &&
     (locale_keys.map(&:to_s) - parsed_csv[0].drop(1)).any?
    errors << :not_all_requested_locales_included_in_header
  end

  # any further checks that we at some time think of should fall here

  fail ArgumentError, errors.map { |e| e.to_s.humanize }.to_sentence if errors.any?
end
validate_yaml() click to toggle source
# File lib/lit/import.rb, line 61
def validate_yaml
  errors = []

  # YAML.load can return false, hence not using #empty?
  errors << :yaml_is_empty if parsed_yaml.blank?

  if parsed_yaml.present? &&
     (locale_keys.map(&:to_sym) - parsed_yaml.keys.map(&:to_sym)).any?
    errors << :not_all_requested_locales_included_in_header
  end

  fail ArgumentError, errors.map { |e| e.to_s.humanize }.to_sentence if errors.any?
end