i18n_yaml_editor 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +60 -0
- data/Rakefile +73 -0
- data/bin/i18n_yaml_editor +7 -0
- data/i18n_yaml_editor.gemspec +70 -0
- data/lib/i18n_yaml_editor/app.rb +125 -0
- data/lib/i18n_yaml_editor/cast.rb +34 -0
- data/lib/i18n_yaml_editor/category.rb +27 -0
- data/lib/i18n_yaml_editor/core_ext.rb +14 -0
- data/lib/i18n_yaml_editor/filter.rb +39 -0
- data/lib/i18n_yaml_editor/key.rb +33 -0
- data/lib/i18n_yaml_editor/store.rb +115 -0
- data/lib/i18n_yaml_editor/transformation.rb +75 -0
- data/lib/i18n_yaml_editor/translation.rb +22 -0
- data/lib/i18n_yaml_editor/update.rb +53 -0
- data/lib/i18n_yaml_editor/web.rb +58 -0
- data/lib/i18n_yaml_editor.rb +16 -0
- data/test/test_helper.rb +23 -0
- data/test/unit/test_app.rb +58 -0
- data/test/unit/test_category.rb +41 -0
- data/test/unit/test_key.rb +46 -0
- data/test/unit/test_store.rb +129 -0
- data/test/unit/test_transformation.rb +45 -0
- data/test/unit/test_translation.rb +16 -0
- data/views/categories.html.erb +9 -0
- data/views/debug.html.erb +9 -0
- data/views/layout.erb +171 -0
- data/views/translations.html.erb +119 -0
- metadata +401 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 59360f083d7e5071393e6605905f034df8340f64
|
4
|
+
data.tar.gz: 422e871b2b24a8061dc75f12721478e6c3e9c4a7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 469c35174ee3ad8f52526830da4ed069fbdb5e4ad37de79dc770d0a5daaf58efc351c98b4e1622f85e24f64a283af1b49f1b49a4d87aa82c49e71e96ef15a436
|
7
|
+
data.tar.gz: e50602ce147daf56bb1837c13678670e86dc46e5848b2cc6824293bb267e4d5ac4b8036579f3c6204836c174aa8dd2e9fc75c798ce1a8ac10841b5cb75067cec
|
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# I18n Yaml Editor
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/Sage/i18n_yaml_editor.svg?branch=master)](https://travis-ci.org/Sage/i18n_yaml_editor) [![Maintainability](https://api.codeclimate.com/v1/badges/f71fb2039dd7d50eb90d/maintainability)](https://codeclimate.com/github/Sage/i18n_yaml_editor/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/f71fb2039dd7d50eb90d/test_coverage)](https://codeclimate.com/github/Sage/i18n_yaml_editor/test_coverage) [![Documentation Status](http://inch-ci.org/github/Sage/i18n_yaml_editor.svg?branch=master)](http://inch-ci.org/github/Sage/i18n_yaml_editor) [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Sage/i18n_yaml_editor/blob/master/LICENSE) [![Dependency Status](https://gemnasium.com/badges/github.com/Sage/i18n_yaml_editor.svg)](https://gemnasium.com/github.com/Sage/i18n_yaml_editor)
|
4
|
+
|
5
|
+
I18n Yaml Editor is based on [IYE](https://github.com/firmafon/iye).
|
6
|
+
I18n Yaml Editor makes it easy to translate your Rails I18N files and keep them up to date.
|
7
|
+
It uses YAML files directly, so you don't need to keep a separate database in sync.
|
8
|
+
This has several benefits:
|
9
|
+
|
10
|
+
* Branching and diffing is trivial
|
11
|
+
* It does not alter the workflow for developers etc., whom can continue editing the
|
12
|
+
YAML files directly
|
13
|
+
* If your YAML files are organized in subfolders, this structure is kept intact
|
14
|
+
|
15
|
+
![I18n Yaml Editor](https://cloud.githubusercontent.com/assets/1446195/10295880/1f829dd6-6bc4-11e5-9a08-bb79d9864bdb.png)
|
16
|
+
|
17
|
+
## Prerequisites
|
18
|
+
|
19
|
+
You need to understand a few things about I18n Yaml Editor for it to make sense, mainly:
|
20
|
+
|
21
|
+
* I18n Yaml Editor does not create new keys - keys must exist for at least one locale in the YAML files
|
22
|
+
* I18n Yaml Editor does not create new locales - at least one key must exist for each locale in the YAML files
|
23
|
+
|
24
|
+
## Workflow
|
25
|
+
|
26
|
+
1. Install I18n Yaml Editor:
|
27
|
+
|
28
|
+
$ git clone git@github.com:Sage/i18n_yaml_editor.git
|
29
|
+
$ cd i18n_yaml_editor
|
30
|
+
$ gem build i18n_yaml_editor.gemspec
|
31
|
+
$ gem install i18n_yaml_editor-1.3.1.gem
|
32
|
+
|
33
|
+
2. The `i18n_yaml_editor` executable is now available, use it wherever you want.
|
34
|
+
|
35
|
+
$ i18n_yaml_editor path/to/i18n/locales [port]
|
36
|
+
|
37
|
+
At this point I18n Yaml Editor loads all translation keys for all locales, and creates any
|
38
|
+
keys that might be missing for existing locales, the default port is 5050
|
39
|
+
|
40
|
+
3. Point browser at [http://localhost:5050](http://localhost:5050)
|
41
|
+
4. Make changes and press 'Save' - each time you do this, all the keys will be
|
42
|
+
written to their original YAML files.
|
43
|
+
5. Review your changes before committing files, e.g. by using `git diff`.
|
44
|
+
|
45
|
+
## Development
|
46
|
+
|
47
|
+
### Getting started
|
48
|
+
$ git clone git@github.com:Sage/i18n_yaml_editor.git
|
49
|
+
$ cd i18n_yaml_editor
|
50
|
+
$ gem install bundler
|
51
|
+
$ bundle install
|
52
|
+
$ SIMPLE_COV=true bundle exec guard
|
53
|
+
|
54
|
+
|
55
|
+
## License
|
56
|
+
|
57
|
+
This gem is available as open source under the terms of the
|
58
|
+
[MIT licence](LICENSE).
|
59
|
+
|
60
|
+
Copyright (c) 2018 Sage Group Plc. All rights reserved.
|
data/Rakefile
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rubocop/rake_task'
|
5
|
+
require 'capture_io/capture_io'
|
6
|
+
|
7
|
+
Rake::TestTask.new do |t|
|
8
|
+
t.libs << 'test'
|
9
|
+
t.libs << 'lib'
|
10
|
+
t.pattern = 'test/**/test_*.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
# Mixin
|
14
|
+
module RuboCop
|
15
|
+
# Mixin
|
16
|
+
class RakeTask
|
17
|
+
private
|
18
|
+
|
19
|
+
def run_cli(verbose, options)
|
20
|
+
require 'rubocop'
|
21
|
+
|
22
|
+
cli = CLI.new
|
23
|
+
puts "\033[1;33m# Running RuboCop...\033[0m" if verbose
|
24
|
+
result = cli.run(options)
|
25
|
+
failed = result.nonzero? && fail_on_error
|
26
|
+
abort("\033[0;31mRuboCop failed!\033[0m\n") if failed
|
27
|
+
puts "\033[0;32mRuboCop passed\033[0m\n\n\n"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'Run RuboCop'
|
33
|
+
RuboCop::RakeTask.new(:rubocop)
|
34
|
+
|
35
|
+
desc 'Generate documentation in doc/ and check documentation coverage'
|
36
|
+
task :yardoc do
|
37
|
+
require 'yard'
|
38
|
+
puts "\n\033[1;33m# Running Yardoc...\033[0m"
|
39
|
+
yard = `yard stats --list-undoc --compact`
|
40
|
+
if yard =~ /Undocumented Objects/
|
41
|
+
puts yard
|
42
|
+
puts "\033[0;31mYardoc failed - documentation coverage < 100%\033[0m\n"
|
43
|
+
exit 1
|
44
|
+
else
|
45
|
+
puts yard.each_line.to_a[0..-2]
|
46
|
+
puts "\033[0;32mYardoc passed - 100.00% documented\033[0m\n\n\n"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
task :coverage do
|
51
|
+
puts "\033[1;33m# Running Minitest...\033[0m"
|
52
|
+
out, = capture_io do
|
53
|
+
puts `bundle exec rake test`
|
54
|
+
end
|
55
|
+
puts out
|
56
|
+
err = /\d+ tests, \d+ assertions, (\d+) failures, (\d+) errors, \d+ skips/
|
57
|
+
unless out.scan(err).flatten.map(&:to_i).reduce(&:+).zero?
|
58
|
+
puts "\n\033[0;31mMinitest failed\033[0m\n"
|
59
|
+
exit 1
|
60
|
+
end
|
61
|
+
|
62
|
+
if out.lines.last =~ /LOC \(100\.0%\) covered/ ||
|
63
|
+
out.lines[-2] =~ /Coverage is at 100\.0%/
|
64
|
+
puts "\033[0;32mMinitest passed - 100.00% covered\033[0m\n\n"
|
65
|
+
else
|
66
|
+
puts "\n\033[0;31mMinitest failed - Code coverage < 100%\033[0m\n"
|
67
|
+
exit 1
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
task default: %i[coverage yardoc rubocop] do
|
72
|
+
puts "\033[0;32mYAY! - I18n Yaml Editor test suite passed\033[0m\n\n"
|
73
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'i18n_yaml_editor'
|
6
|
+
s.version = '2.0.0'
|
7
|
+
s.date = '2015-10-07'
|
8
|
+
s.summary = 'I18n Yaml Editor'
|
9
|
+
s.email = 'wolfgang.teuber@sage.com'
|
10
|
+
s.homepage = 'http://github.com/Sage/i18n_yaml_editor'
|
11
|
+
s.description = 'I18n Yaml Editor'
|
12
|
+
s.authors = ['Harry Vangberg', 'Wolfgang Teuber']
|
13
|
+
s.executables << 'i18n_yaml_editor'
|
14
|
+
s.files = [
|
15
|
+
'README.md',
|
16
|
+
'Rakefile',
|
17
|
+
'i18n_yaml_editor.gemspec',
|
18
|
+
'bin/i18n_yaml_editor',
|
19
|
+
'lib/i18n_yaml_editor.rb',
|
20
|
+
'lib/i18n_yaml_editor/app.rb',
|
21
|
+
'lib/i18n_yaml_editor/category.rb',
|
22
|
+
'lib/i18n_yaml_editor/core_ext.rb',
|
23
|
+
'lib/i18n_yaml_editor/key.rb',
|
24
|
+
'lib/i18n_yaml_editor/store.rb',
|
25
|
+
'lib/i18n_yaml_editor/filter.rb',
|
26
|
+
'lib/i18n_yaml_editor/update.rb',
|
27
|
+
'lib/i18n_yaml_editor/cast.rb',
|
28
|
+
'lib/i18n_yaml_editor/transformation.rb',
|
29
|
+
'lib/i18n_yaml_editor/translation.rb',
|
30
|
+
'lib/i18n_yaml_editor/web.rb',
|
31
|
+
'views/categories.html.erb',
|
32
|
+
'views/debug.html.erb',
|
33
|
+
'views/layout.erb',
|
34
|
+
'views/translations.html.erb'
|
35
|
+
]
|
36
|
+
s.test_files = [
|
37
|
+
'test/test_helper.rb',
|
38
|
+
'test/unit/test_app.rb',
|
39
|
+
'test/unit/test_category.rb',
|
40
|
+
'test/unit/test_key.rb',
|
41
|
+
'test/unit/test_store.rb',
|
42
|
+
'test/unit/test_transformation.rb',
|
43
|
+
'test/unit/test_translation.rb'
|
44
|
+
]
|
45
|
+
s.add_dependency 'activesupport', '>= 4.0.2'
|
46
|
+
s.add_dependency 'cuba', '>= 3'
|
47
|
+
s.add_dependency 'psych', '>= 1.3.4'
|
48
|
+
s.add_dependency 'tilt', '>= 1.3'
|
49
|
+
|
50
|
+
s.add_development_dependency 'awesome_print'
|
51
|
+
s.add_development_dependency 'cane'
|
52
|
+
s.add_development_dependency 'flay'
|
53
|
+
s.add_development_dependency 'flog'
|
54
|
+
s.add_development_dependency 'github-markup'
|
55
|
+
s.add_development_dependency 'guard'
|
56
|
+
s.add_development_dependency 'guard-minitest'
|
57
|
+
s.add_development_dependency 'guard-rubocop'
|
58
|
+
s.add_development_dependency 'inch'
|
59
|
+
s.add_development_dependency 'minitest-line'
|
60
|
+
s.add_development_dependency 'minitest-reporters'
|
61
|
+
s.add_development_dependency 'pry-byebug'
|
62
|
+
s.add_development_dependency 'pry-doc'
|
63
|
+
s.add_development_dependency 'rack-test'
|
64
|
+
s.add_development_dependency 'rake'
|
65
|
+
s.add_development_dependency 'redcarpet'
|
66
|
+
s.add_development_dependency 'rubocop'
|
67
|
+
s.add_development_dependency 'simplecov'
|
68
|
+
s.add_development_dependency 'yard'
|
69
|
+
end
|
70
|
+
# rubocop:enable Metrics/BlockLength
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'psych'
|
4
|
+
require 'yaml'
|
5
|
+
require 'active_support/all'
|
6
|
+
|
7
|
+
require 'i18n_yaml_editor/web'
|
8
|
+
require 'i18n_yaml_editor/store'
|
9
|
+
require 'i18n_yaml_editor/core_ext'
|
10
|
+
|
11
|
+
module I18nYamlEditor
|
12
|
+
# App provides I18n Yaml Editor's top-level functionality:
|
13
|
+
# * Starting I18n Yaml Editor
|
14
|
+
# * Loading Translation files
|
15
|
+
# * Saving Translation files
|
16
|
+
class App
|
17
|
+
attr_accessor :store
|
18
|
+
|
19
|
+
def initialize(path, port = 5050)
|
20
|
+
@path = File.expand_path(path)
|
21
|
+
@port = port || 5050
|
22
|
+
@store = Store.new
|
23
|
+
I18nYamlEditor.app = self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Starts I18n Yaml Editor server
|
27
|
+
def start
|
28
|
+
raise "File #{@path} not found." unless File.exist?(@path)
|
29
|
+
$stdout.puts " * Loading translations from #{@path}"
|
30
|
+
load_translations
|
31
|
+
|
32
|
+
$stdout.puts ' * Creating missing translations'
|
33
|
+
store.create_missing_keys
|
34
|
+
|
35
|
+
$stdout.puts " * Starting I18n Yaml Editor at port #{@port}"
|
36
|
+
Rack::Server.start app: Web, Port: @port
|
37
|
+
end
|
38
|
+
|
39
|
+
# Loads translations from a given path
|
40
|
+
def load_translations
|
41
|
+
files = if File.directory?(@path)
|
42
|
+
Dir[@path + '/**/*.yml']
|
43
|
+
elsif File.file?(@path)
|
44
|
+
detect_list_or_file @path
|
45
|
+
else
|
46
|
+
# rubocop:disable Style/StderrPuts
|
47
|
+
$stderr.puts 'No valid translation file given'
|
48
|
+
# rubocop:enable Style/StderrPuts
|
49
|
+
[]
|
50
|
+
end
|
51
|
+
update_store files
|
52
|
+
end
|
53
|
+
|
54
|
+
# Write the given translations to the appropriate YAML file
|
55
|
+
# example translations:
|
56
|
+
# {"en.day_names"=>"Mon\r\nTue", "en.session.new.password"=>"Password"}
|
57
|
+
def save_translations(translations)
|
58
|
+
store.update(translations)
|
59
|
+
changes = files(translations: translations)
|
60
|
+
changes.each do |file, yaml|
|
61
|
+
File.open(file, 'w', encoding: 'utf-8') do |f|
|
62
|
+
f.puts normalize(yaml)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# Enables I18n Yaml Editor to deal with inputs that reference a file list or
|
70
|
+
# a single translation file
|
71
|
+
def detect_list_or_file(path)
|
72
|
+
file = YAML.load_file(path)
|
73
|
+
file.is_a?(Hash) ? [path] : File.read(path).split
|
74
|
+
end
|
75
|
+
|
76
|
+
def update_store(files)
|
77
|
+
files.each do |file|
|
78
|
+
if File.exist?(file)
|
79
|
+
yaml = YAML.load_file(file)
|
80
|
+
store.from_yaml(yaml, file)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def files(translations: {})
|
86
|
+
store.to_yaml.select do |_, i18n_hash|
|
87
|
+
translations.keys.any? do |i18n_key|
|
88
|
+
key_in_i18n_hash? i18n_key, i18n_hash
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def key_in_i18n_hash?(i18n_key, i18n_hash)
|
94
|
+
!i18n_key.split('.').inject(i18n_hash) do |hash, k|
|
95
|
+
begin
|
96
|
+
hash[k]
|
97
|
+
rescue StandardError
|
98
|
+
{}
|
99
|
+
end
|
100
|
+
end.nil?
|
101
|
+
end
|
102
|
+
|
103
|
+
def normalize(yaml)
|
104
|
+
i18n_yaml = yaml.with_indifferent_access.to_hash.to_yaml
|
105
|
+
i18n_yaml_lines = i18n_yaml.split(/\n/).reject { |e| e == '' }[1..-1]
|
106
|
+
normalize_empty_lines(i18n_yaml_lines) * "\n"
|
107
|
+
end
|
108
|
+
|
109
|
+
def normalize_empty_lines(i18n_yaml_lines)
|
110
|
+
yaml_ary = []
|
111
|
+
i18n_yaml_lines.each_with_index do |line, idx|
|
112
|
+
yaml_ary << line
|
113
|
+
yaml_ary << '' if add_empty_line?(i18n_yaml_lines, line, idx)
|
114
|
+
end
|
115
|
+
yaml_ary
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_empty_line?(process, line, idx)
|
119
|
+
return if process[idx + 1].nil?
|
120
|
+
this_line_spcs = line[/\A\s*/].length
|
121
|
+
next_line_spcs = process[idx + 1][/\A\s*/].length
|
122
|
+
this_line_spcs - next_line_spcs > 2
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nYamlEditor
|
4
|
+
# Transformation provides
|
5
|
+
module Cast
|
6
|
+
# Lists all supported types of conversions
|
7
|
+
TYPES = %i[numeric boolean array].freeze
|
8
|
+
|
9
|
+
# Contains a check method for each supported type
|
10
|
+
CHECK = {
|
11
|
+
numeric: ->(klass) { klass < Numeric },
|
12
|
+
boolean: ->(klass) { [TrueClass, FalseClass].include?(klass) },
|
13
|
+
array: ->(klass) { klass == Array }
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
# Contains a conversion method for each supported type
|
17
|
+
CONVERT = {
|
18
|
+
numeric: lambda do |value|
|
19
|
+
num = BigDecimal(value)
|
20
|
+
num.frac.zero? ? num.to_i : num.to_f
|
21
|
+
end,
|
22
|
+
boolean: ->(value) { value.casecmp('true').zero? },
|
23
|
+
array: ->(value) { value.split("\r\n") }
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
# Converts a given value to a specific data type
|
27
|
+
def cast(klass, value)
|
28
|
+
TYPES.each do |type|
|
29
|
+
return CONVERT[type].call(value) if CHECK[type].call(klass)
|
30
|
+
end
|
31
|
+
value.to_s # String, blank
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module I18nYamlEditor
|
6
|
+
# This is a category
|
7
|
+
class Category
|
8
|
+
attr_accessor :name, :keys
|
9
|
+
|
10
|
+
def initialize(attributes = {})
|
11
|
+
@name = attributes[:name]
|
12
|
+
@keys = Set.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Adds a given key to this category's list of keys
|
16
|
+
# @param key [Key] key to be added to this category's list of keys
|
17
|
+
def add_key(key)
|
18
|
+
keys.add(key)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Checks and returns if all keys are complete
|
22
|
+
# @return [true, false]
|
23
|
+
def complete?
|
24
|
+
keys.all?(&:complete?)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Extend Hash with sort_by_key method
|
4
|
+
class Hash
|
5
|
+
# Sorts entries alphabetically by key
|
6
|
+
def sort_by_key(recursive = false, &block)
|
7
|
+
keys.sort(&block).each_with_object({}) do |key, seed|
|
8
|
+
seed[key] = self[key]
|
9
|
+
if recursive && seed[key].is_a?(Hash)
|
10
|
+
seed[key] = seed[key].sort_by_key(true, &block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nYamlEditor
|
4
|
+
# Transformation provides
|
5
|
+
module Filter
|
6
|
+
# Selects keys from this store according to the given filter options
|
7
|
+
def filter_keys(options = {})
|
8
|
+
filters = filters(options)
|
9
|
+
keys.select { |_, key| filters.all? { |filter| filter.call(key) } }
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def filters(options)
|
15
|
+
list = []
|
16
|
+
list << key_filter(options) if options.key?(:key)
|
17
|
+
list << complete_filter(options) if options.key?(:complete)
|
18
|
+
list << empty_filter(options) if options.key?(:empty)
|
19
|
+
list << text_filter(options) if options.key?(:text)
|
20
|
+
list
|
21
|
+
end
|
22
|
+
|
23
|
+
def key_filter(options)
|
24
|
+
->(k) { k.name =~ options[:key] }
|
25
|
+
end
|
26
|
+
|
27
|
+
def complete_filter(options)
|
28
|
+
->(k) { k.complete? == options[:complete] }
|
29
|
+
end
|
30
|
+
|
31
|
+
def empty_filter(options)
|
32
|
+
->(k) { k.empty? == options[:empty] }
|
33
|
+
end
|
34
|
+
|
35
|
+
def text_filter(options)
|
36
|
+
->(k) { k.translations.any? { |t| t.text =~ options[:text] } }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module I18nYamlEditor
|
6
|
+
# This is a key
|
7
|
+
class Key
|
8
|
+
attr_accessor :name, :translations
|
9
|
+
|
10
|
+
def initialize(attributes = {})
|
11
|
+
@name = attributes[:name]
|
12
|
+
@translations = Set.new
|
13
|
+
end
|
14
|
+
|
15
|
+
# Adds a translation to this key's list of translations
|
16
|
+
def add_translation(translation)
|
17
|
+
translations.add(translation)
|
18
|
+
end
|
19
|
+
|
20
|
+
# This key's category
|
21
|
+
def category
|
22
|
+
@category ||= name.split('.').first
|
23
|
+
end
|
24
|
+
|
25
|
+
def complete?
|
26
|
+
translations.all? { |t| t.text.to_s !~ /\A\s*\z/ } || empty?
|
27
|
+
end
|
28
|
+
|
29
|
+
def empty?
|
30
|
+
translations.all? { |t| t.text.to_s =~ /\A\s*\z/ }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
require 'i18n_yaml_editor/transformation'
|
7
|
+
require 'i18n_yaml_editor/filter'
|
8
|
+
require 'i18n_yaml_editor/update'
|
9
|
+
require 'i18n_yaml_editor/category'
|
10
|
+
require 'i18n_yaml_editor/key'
|
11
|
+
require 'i18n_yaml_editor/translation'
|
12
|
+
|
13
|
+
module I18nYamlEditor
|
14
|
+
# Raised when translation entries have the same key
|
15
|
+
class DuplicateTranslationError < StandardError; end
|
16
|
+
|
17
|
+
# Store keeps all i18n data
|
18
|
+
class Store
|
19
|
+
include Transformation
|
20
|
+
include Filter
|
21
|
+
include Update
|
22
|
+
|
23
|
+
attr_accessor :categories, :keys, :translations, :locales
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@categories = {}
|
27
|
+
@keys = {}
|
28
|
+
@translations = {}
|
29
|
+
@locales = Set.new
|
30
|
+
end
|
31
|
+
|
32
|
+
# Adds a given translation to the store
|
33
|
+
def add_translation(translation)
|
34
|
+
check_duplication! translation
|
35
|
+
|
36
|
+
translations[translation.name] = translation
|
37
|
+
|
38
|
+
locales.add(translation.locale)
|
39
|
+
key = init_key(translation)
|
40
|
+
init_category(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Generates a new key with the given translation
|
44
|
+
def init_key(translation)
|
45
|
+
key = (keys[translation.key] ||= Key.new(name: translation.key))
|
46
|
+
key.add_translation(translation)
|
47
|
+
key
|
48
|
+
end
|
49
|
+
|
50
|
+
# Generates a new category with the given key
|
51
|
+
def init_category(key)
|
52
|
+
category = (categories[key.category] ||= Category.new(name: key.category))
|
53
|
+
category.add_key(key)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Adds a key to this store
|
57
|
+
def add_key(key)
|
58
|
+
keys[key.name] = key
|
59
|
+
end
|
60
|
+
|
61
|
+
# Creates all keys for each locale that doesn't have all keys from all
|
62
|
+
# other locales
|
63
|
+
def create_missing_keys
|
64
|
+
keys.each do |_name, key|
|
65
|
+
missing_locales = locales - key.translations.map(&:locale)
|
66
|
+
missing_locales.each do |locale|
|
67
|
+
translation = key.translations.first
|
68
|
+
name = "#{locale}.#{key.name}"
|
69
|
+
path = translation_path locale, translation
|
70
|
+
add_translation(Translation.new(name: name, file: path))
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Adds a translation for every entry in a given yaml hash
|
76
|
+
def from_yaml(yaml, file = nil)
|
77
|
+
translations = flatten_hash(yaml)
|
78
|
+
translations.each do |name, text|
|
79
|
+
translation = Translation.new(name: name, text: text, file: file)
|
80
|
+
add_translation(translation)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Returns a hash with the structure of a i18n
|
85
|
+
def to_yaml
|
86
|
+
result = {}
|
87
|
+
files = translations.values.group_by(&:file)
|
88
|
+
files.each do |file, translations|
|
89
|
+
file_result = {}
|
90
|
+
translations.each do |translation|
|
91
|
+
file_result[translation.name] = translation.text
|
92
|
+
end
|
93
|
+
result[file] = nest_hash(file_result)
|
94
|
+
end
|
95
|
+
result
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def translation_path(locale, translation)
|
101
|
+
translation.file.gsub(/(.*)[A-z]{2}(\.yml)\z/, "\\1#{locale}\\2")
|
102
|
+
end
|
103
|
+
|
104
|
+
def check_duplication!(translation)
|
105
|
+
existing = translations[translation.name]
|
106
|
+
return unless existing
|
107
|
+
error_message = error_message(translation, existing)
|
108
|
+
raise DuplicateTranslationError, error_message
|
109
|
+
end
|
110
|
+
|
111
|
+
def error_message(translation, existing)
|
112
|
+
"#{translation.name} detected in #{translation.file} and #{existing.file}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nYamlEditor
|
4
|
+
# Raised when nesting of I18n keys to a Hash fails
|
5
|
+
class TransformationError < StandardError; end
|
6
|
+
|
7
|
+
# Transformation provides
|
8
|
+
module Transformation
|
9
|
+
# Public: Converts a deep hash to one level by generating new keys
|
10
|
+
# by joining the previous key path to the value with a '.'
|
11
|
+
#
|
12
|
+
# hash - The original Hash.
|
13
|
+
# namespace - An optional namespace, default: [].
|
14
|
+
# tree - An optional tree to add values to, default: {}.
|
15
|
+
#
|
16
|
+
# Examples
|
17
|
+
# flatten_hash({da: {session: { login: 'Log ind', logout: 'Log ud' }}})
|
18
|
+
# # => {"da.session.login"=>"Log ind", "da.session.logout"=>"Log ud"}
|
19
|
+
#
|
20
|
+
# Returns the generated Hash.
|
21
|
+
def flatten_hash(hash, namespace = [], tree = {})
|
22
|
+
hash.each do |key, value|
|
23
|
+
child_ns = namespace.dup << key
|
24
|
+
if value.is_a?(Hash)
|
25
|
+
flatten_hash value, child_ns, tree
|
26
|
+
else
|
27
|
+
tree[child_ns.join('.')] = value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
tree
|
31
|
+
end
|
32
|
+
module_function :flatten_hash
|
33
|
+
|
34
|
+
# Public: Converts a flat hash with key path to the value joined
|
35
|
+
# with a '.' to a one level Hash, it's the reverse of flatten_hash
|
36
|
+
#
|
37
|
+
# hash - Hash with keys that represent the path to the value in the new Hash
|
38
|
+
#
|
39
|
+
# Examples
|
40
|
+
# nest_hash({"da.session.login"=>"Log ind", "da.session.logout"=>"Log ud"})
|
41
|
+
# # => {"da"=>{"session"=>{"login"=>"Log ind", "logout"=>"Log ud"}}}
|
42
|
+
#
|
43
|
+
# Returns the generated Hash.
|
44
|
+
def nest_hash(hash)
|
45
|
+
result = {}
|
46
|
+
hash.each do |key, value|
|
47
|
+
begin
|
48
|
+
nest_key result, key, value
|
49
|
+
rescue StandardError
|
50
|
+
raise TransformationError,
|
51
|
+
"Failed to nest key: #{key.inspect} with #{value.inspect}"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
result
|
55
|
+
end
|
56
|
+
module_function :nest_hash
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Recursively nests point-separated keys from a string in a hash and
|
61
|
+
# assigns the given value to this new hash entry
|
62
|
+
def nest_key(result, key, value)
|
63
|
+
sub_result = result
|
64
|
+
keys = key.split('.')
|
65
|
+
keys.each_with_index do |k, idx|
|
66
|
+
if keys.size - 1 == idx
|
67
|
+
sub_result[k] = value
|
68
|
+
else
|
69
|
+
sub_result = (sub_result[k] ||= {})
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
module_function :nest_key
|
74
|
+
end
|
75
|
+
end
|