Module: ImmosquareYaml

Extended by:
SharedMethods
Defined in:
lib/immosquare-yaml.rb,
lib/immosquare-yaml/delete.rb,
lib/immosquare-yaml/flatten.rb,
lib/immosquare-yaml/version.rb,
lib/immosquare-yaml/shared_methods.rb

Overview

##

ImmosquareYaml — post-processeur Psych dédié aux fichiers de traduction (locales Rails).

Trois responsabilités :

- parse(file)  : YAML → Hash, en s'appuyant sur l'AST Psych
- dump(hash)   : Hash → YAML formaté (quotes minimales,
                 blocs littéraux, emojis décodés)
- clean(file)  : parse + tri par clé + dump → écrit

La gem résout cinq problèmes que Psych seul ne traite pas :

1. Norway problem (yes/no/on/off lus comme String)
2. Tri déterministe par clé
3. Préservation des blocs littéraux (|, |-)
4. Quotes minimales pour la lisibilité
5. Décodage des escapes \U0001F600 → emoji
##

Defined Under Namespace

Modules: SharedMethods

Constant Summary collapse

VERSION =
"1.0.3".freeze

Constants included from SharedMethods

SharedMethods::CUSTOM_SEPARATOR, SharedMethods::DOUBLE_QUOTE, SharedMethods::DOUBLE_SIMPLE_QUOTE, SharedMethods::INDENT_SIZE, SharedMethods::NEWLINE, SharedMethods::NOTHING, SharedMethods::RESERVED_KEYS, SharedMethods::SIMPLE_QUOTE, SharedMethods::SPACE, SharedMethods::WEIRD_QUOTES_REGEX, SharedMethods::YML_SPECIAL_CHARS

Class Method Summary collapse

Methods included from SharedMethods

deep_transform_values

Class Method Details

.clean(file_path, **options) ⇒ Object

##

clean(file_path, sort: true, output: file_path) Charge le fichier, le re-écrit propre et trié. Retourne true / false selon le succès.

##


35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/immosquare-yaml.rb', line 35

def clean(file_path, **options)
  options = {
    :sort   => true,
    :output => file_path
  }.merge(options)

  begin
    raise("File not found") if !File.exist?(file_path)

    parsed_yml = parse(file_path, :sort => options[:sort])
    return false if parsed_yml == false

    output = dump(parsed_yml)
    FileUtils.mkdir_p(File.dirname(options[:output]))
    File.write(options[:output], output)
    true
  rescue StandardError => e
    puts(e.message)
    puts(e.backtrace)
    false
  end
end

.delete_paths(file_path, paths, **options) ⇒ Object

##

delete_paths(file_path, paths, sort: true, output: file_path)

paths : a single dot-path String or an Array<String>.

Returns a Hash :

{ :deleted   => [paths actually removed],
  :not_found => [paths that did not exist in the file] }

Returns false if the file cannot be parsed or if its root node is not a Hash (i18n YAML files are always mappings).

Note : the file is always rewritten through ‘dump`, even when every path is reported as :not_found. This means calling `delete_paths` doubles as a `clean` (sort + reformat) on the target file. Use `:output => “…”` to write elsewhere.

##


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/immosquare-yaml/delete.rb', line 32

def delete_paths(file_path, paths, **options)
  options = {
    :sort   => true,
    :output => file_path
  }.merge(options)

  begin
    raise("File not found") if !File.exist?(file_path)

    parsed = parse(file_path, :sort => options[:sort])
    return false if parsed == false || !parsed.is_a?(Hash)

    paths     = Array(paths)
    deleted   = []
    not_found = []

    ##============================================================##
    ## Walk each dot-path, delete the leaf if present, prune
    ## empty parents on the way back up.
    ##============================================================##
    paths.each do |dot_path|
      segments = parse_path(dot_path)
      if !segments.empty? && delete_at_segments(parsed, segments)
        deleted << dot_path
      else
        not_found << dot_path
      end
    end

    output = dump(parsed)
    FileUtils.mkdir_p(File.dirname(options[:output]))
    File.write(options[:output], output)

    {:deleted => deleted, :not_found => not_found}
  rescue StandardError => e
    puts(e.message)
    puts(e.backtrace)
    false
  end
end

.dump(hash) ⇒ Object

##

dump(hash) → String YAML Sérialise un Hash en YAML avec nos règles de formatage :

- clés "yes/no/on/..." re-quotées
- valeurs plain quand c'est sûr, sinon doublequotées
- chaînes multi-lignes en bloc littéral | ou |-
- arrays imbriqués délégués à Psych.dump puis indentés
##


106
107
108
# File 'lib/immosquare-yaml.rb', line 106

def dump(hash)
  render_hash(hash, [], 0)
end

.flatten_keys(input, **options) ⇒ Object

##

flatten_keys(input, with_values: false, with_file: false)

input :

- Hash             : flattened in memory (no I/O). File
                     column is nil in the resulting tuples.
- String           : a single YAML file path.
- Array<String>    : a list of YAML file paths.

Globs are NOT expanded — the caller is expected to expand them upstream (e.g. with Dir.glob) and pass an Array<String>. Mixing a Hash with file paths in the same Array is not supported.

No option → Array<String> sorted + deduplicated with_values: true → Array<[path, value]> not deduplicated with_file: true → adds the source file_path to each entry

(nil for Hash inputs)

Missing / empty / unreadable file: silently ignored.

##


38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/immosquare-yaml/flatten.rb', line 38

def flatten_keys(input, **options)
  options = {
    :with_values => false,
    :with_file   => false
  }.merge(options)

  entries = []

  if input.is_a?(Hash)
    flatten_hash(input, [], entries, nil) if !input.empty?
  else
    paths = input.is_a?(Array) ? input : [input]
    paths.each do |path|
      path = path.to_s
      next if path.empty? || !File.exist?(path)

      parsed = parse(path, :sort => false)
      next if parsed == false || !parsed.is_a?(Hash) || parsed.empty?

      flatten_hash(parsed, [], entries, path)
    end
  end

  format_entries(entries, options)
end

.parse(file_path, **options) ⇒ Object

##

parse(file_path, sort: true) Lit un fichier YAML et retourne un Hash Ruby. Hash trié par clé par défaut.

Implémentation : on parcourt l’AST Psych plutôt que d’appeler Psych.load. Cela permet de :

- distinguer un scalaire plain "yes" d'un bool true
- garder les valeurs problématiques (Norway) en String
- décoder nous-mêmes les escapes \U... pour les blocs
  littéraux qui ne sont pas désescapés par Psych
##


70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/immosquare-yaml.rb', line 70

def parse(file_path, **options)
  options = {:sort => true}.merge(options)

  begin
    raise("File not found") if !File.exist?(file_path)

    ##============================================================##
    ## Psych.parse_file retourne un Document. Si le fichier est
    ## vide ou ne contient que des commentaires, root est nil.
    ##============================================================##
    doc = Psych.parse_file(file_path)
    return {} if !doc || doc.root.nil?

    result = node_to_value(doc.root, {})

    ##============================================================##
    ## On accepte tous les types racine (Hash, Array, scalaire),
    ## mais on ne trie que si la racine est un Hash.
    ##============================================================##
    result = result.sort_by_key if options[:sort] && result.is_a?(Hash)
    result
  rescue StandardError => e
    puts(e.message)
    puts(e.backtrace)
    false
  end
end

.parse_path(dot_path) ⇒ Object

##

parse_path(dot_path) → Array<String>

Symmetric inverse of the quoting done by flatten_keys: splits on “.” and strips wrapping quotes from reserved or numeric segments. The result can be passed directly to Hash#dig on a hash returned by ImmosquareYaml.parse.

Limitation : keys containing a literal “.” in their name are not supported (the path would be split into two segments).

Examples :

parse_path("fr.statuses.\"yes\"")  # => ["fr", "statuses", "yes"]
parse_path("fr.counts.\"42\"")     # => ["fr", "counts", "42"]
parse_path("fr.app.title")         # => ["fr", "app", "title"]
##


80
81
82
# File 'lib/immosquare-yaml/flatten.rb', line 80

def parse_path(dot_path)
  dot_path.to_s.split(".").map {|segment| unquote_segment(segment) }
end