Class: Multilocale::LocaleFile

Inherits:
Object
  • Object
show all
Defined in:
lib/multilocale/locale_file.rb

Overview

Reads and writes the locale files on disk.

The path is a template containing %lang%, the same convention the npm CLI uses in multilocale.json:

config/locales/%lang%.yml  ->  config/locales/en.yml, config/locales/es.yml, …

YAML is written the way the i18n gem expects it: a single top-level locale key, nested below it. Two differences from npx multilocale download are deliberate and both matter to Ruby:

* no injected "locale" entry. The CLI writes one into every dictionary it
generates (only its Swift writer strips it again); in a Rails locale
file it would surface as the translation `t("locale")`.
* dotted keys are nested, because i18n resolves them by walking hashes.

Constant Summary collapse

FORMATS =
%i[yaml json].freeze
PLACEHOLDER =
"%lang%"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(path_template:, format: nil, nested: true, header: nil, base_dir: Dir.pwd) ⇒ LocaleFile

Returns a new instance of LocaleFile.



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/multilocale/locale_file.rb', line 29

def initialize(path_template:, format: nil, nested: true, header: nil, base_dir: Dir.pwd)
  unless path_template.to_s.include?(PLACEHOLDER)
    raise ConfigurationError, "Path #{path_template.inspect} must contain #{PLACEHOLDER}, e.g. " \
                              "config/locales/#{PLACEHOLDER}.yml"
  end

  @path_template = path_template.to_s
  @format = (format || self.class.infer_format(@path_template)).to_sym

  unless FORMATS.include?(@format)
    raise ConfigurationError,
          "Unsupported format #{@format.inspect} (supported: #{FORMATS.join(', ')}). " \
          "The npm CLI's cjs/esm/js/swift writers have no equivalent here."
  end

  @nested = nested
  @header = header
  @base_dir = base_dir
end

Instance Attribute Details

#base_dirObject (readonly)

Returns the value of attribute base_dir.



27
28
29
# File 'lib/multilocale/locale_file.rb', line 27

def base_dir
  @base_dir
end

#formatObject (readonly)

Returns the value of attribute format.



27
28
29
# File 'lib/multilocale/locale_file.rb', line 27

def format
  @format
end

#headerObject (readonly)

Returns the value of attribute header.



27
28
29
# File 'lib/multilocale/locale_file.rb', line 27

def header
  @header
end

#nestedObject (readonly)

Returns the value of attribute nested.



27
28
29
# File 'lib/multilocale/locale_file.rb', line 27

def nested
  @nested
end

#path_templateObject (readonly)

Returns the value of attribute path_template.



27
28
29
# File 'lib/multilocale/locale_file.rb', line 27

def path_template
  @path_template
end

Class Method Details

.infer_format(path) ⇒ Object



49
50
51
52
53
54
# File 'lib/multilocale/locale_file.rb', line 49

def self.infer_format(path)
  case File.extname(path).downcase
  when ".json" then :json
  else :yaml
  end
end

Instance Method Details

#path_for(language) ⇒ Object



56
57
58
# File 'lib/multilocale/locale_file.rb', line 56

def path_for(language)
  File.expand_path(path_template.gsub(PLACEHOLDER, language.to_s), base_dir)
end

#read(language) ⇒ Object

Reads one language file back into a Dictionary, flattening the nesting. Returns nil when the file does not exist.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/multilocale/locale_file.rb', line 85

def read(language)
  path = path_for(language)
  return nil unless File.exist?(path)

  raw = File.read(path)
  document =
    case format
    when :yaml then YAML.safe_load(raw) || {}
    else JSON.parse(raw)
    end

  # A locale file is `{ "en" => { … } }`; anything else is either already
  # flat or someone else's file, and is taken as it comes.
  body =
    if document.is_a?(Hash) && document.size == 1 && document.key?(language.to_s)
      document[language.to_s]
    else
      document
    end

  Dictionary.new(language.to_s, Dictionary.flatten(body))
end

#render(dictionary) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/multilocale/locale_file.rb', line 68

def render(dictionary)
  body = nested ? dictionary.nested : dictionary.to_h

  case format
  when :yaml
    # line_width: -1 keeps long sentences on one line; wrapped YAML is
    # valid but re-wraps on every unrelated edit and ruins the diff.
    "#{yaml_header}#{{ dictionary.language.to_s => body }.to_yaml(line_width: -1)}"
  else
    # JSON has no comment syntax, so `header` is deliberately dropped here
    # rather than written as an invalid first line.
    "#{JSON.pretty_generate(body)}\n"
  end
end

#write(dictionary) ⇒ Object

Writes one language and returns the absolute path written.



61
62
63
64
65
66
# File 'lib/multilocale/locale_file.rb', line 61

def write(dictionary)
  path = path_for(dictionary.language)
  FileUtils.mkdir_p(File.dirname(path))
  File.write(path, render(dictionary))
  path
end