i18n_yaml_editor 2.0.0
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 +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
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module I18nYamlEditor
|
4
|
+
# A Translation holds information about its name, file and text
|
5
|
+
class Translation
|
6
|
+
attr_accessor :name, :file, :text
|
7
|
+
|
8
|
+
def initialize(attributes = {})
|
9
|
+
@name, @file, @text = attributes.values_at(:name, :file, :text)
|
10
|
+
end
|
11
|
+
|
12
|
+
# This translation's key
|
13
|
+
def key
|
14
|
+
@key ||= name.split('.')[1..-1].join('.')
|
15
|
+
end
|
16
|
+
|
17
|
+
# This translation's locale
|
18
|
+
def locale
|
19
|
+
@locale ||= name.split('.').first
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'i18n_yaml_editor/cast'
|
4
|
+
|
5
|
+
module I18nYamlEditor
|
6
|
+
# Provides update functionality for Store
|
7
|
+
module Update
|
8
|
+
include Cast
|
9
|
+
|
10
|
+
# Update translations in store
|
11
|
+
def update(new_translations)
|
12
|
+
translations_in_store = translations.values.group_by(&:key)
|
13
|
+
|
14
|
+
new_translations.each do |name, text|
|
15
|
+
if translations[name]
|
16
|
+
update_translation(name, text, translations_in_store)
|
17
|
+
else
|
18
|
+
# rubocop:disable Style/StderrPuts
|
19
|
+
$stderr.puts 'Translation not found'
|
20
|
+
# rubocop:enable Style/StderrPuts
|
21
|
+
next
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def update_array(obj, name, text)
|
29
|
+
klasses = obj.map(&:class).uniq!
|
30
|
+
return unless klasses.length < 2
|
31
|
+
translations[name].text = []
|
32
|
+
cast(Array, text).each do |item|
|
33
|
+
translations[name].text << cast(klasses[0], item)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def update_with_type(obj, name, text)
|
38
|
+
data_type = obj.class
|
39
|
+
data_type = String if data_type == NilClass
|
40
|
+
if data_type == Array
|
41
|
+
update_array(obj, name, text)
|
42
|
+
else
|
43
|
+
translations[name].text = cast(data_type, text)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def update_translation(name, text, translations_in_store)
|
48
|
+
objs = translations_in_store[translations[name].key].map(&:text)
|
49
|
+
obj = objs.find { |t| t.present? || t == false }
|
50
|
+
update_with_type(obj, name, text)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cuba'
|
4
|
+
require 'cuba/render'
|
5
|
+
require 'bigdecimal'
|
6
|
+
|
7
|
+
module I18nYamlEditor
|
8
|
+
# The frontend rendering engine
|
9
|
+
class Web < Cuba
|
10
|
+
plugin Cuba::Render
|
11
|
+
|
12
|
+
settings[:render][:template_engine] = 'erb'
|
13
|
+
settings[:render][:views] = File.expand_path(
|
14
|
+
File.join(File.dirname(__FILE__), '..', '..', 'views')
|
15
|
+
)
|
16
|
+
|
17
|
+
use Rack::ShowExceptions
|
18
|
+
|
19
|
+
# Reads global App instance
|
20
|
+
def app
|
21
|
+
I18nYamlEditor.app
|
22
|
+
end
|
23
|
+
|
24
|
+
define do
|
25
|
+
on get, root do
|
26
|
+
on param('filters') do |filters|
|
27
|
+
opts = {}
|
28
|
+
opts[:key] = /#{filters["key"]}/ unless filters['key'].to_s.empty?
|
29
|
+
opts[:text] = /#{filters["text"]}/i unless filters['text'].to_s.empty?
|
30
|
+
opts[:complete] = false if filters['incomplete'] == 'on'
|
31
|
+
opts[:empty] = true if filters['empty'] == 'on'
|
32
|
+
|
33
|
+
keys = app.store.filter_keys(opts)
|
34
|
+
|
35
|
+
res.write view('translations.html', keys: keys, filters: filters)
|
36
|
+
end
|
37
|
+
|
38
|
+
on default do
|
39
|
+
categories = app.store.categories.sort
|
40
|
+
res.write view('categories.html', categories: categories, filters: {})
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
on post, 'update' do
|
45
|
+
translations = req['translations']
|
46
|
+
app.save_translations(translations) if translations
|
47
|
+
|
48
|
+
url = Rack::Utils.build_nested_query(filters: req['filters'])
|
49
|
+
res.redirect "/?#{url}"
|
50
|
+
end
|
51
|
+
|
52
|
+
on get, 'debug' do
|
53
|
+
res.write partial('debug.html',
|
54
|
+
translations: app.store.translations.values)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# I18n Yaml Editor
|
4
|
+
module I18nYamlEditor
|
5
|
+
class << self
|
6
|
+
attr_accessor :app
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'i18n_yaml_editor/app'
|
11
|
+
require 'i18n_yaml_editor/category'
|
12
|
+
require 'i18n_yaml_editor/key'
|
13
|
+
require 'i18n_yaml_editor/store'
|
14
|
+
require 'i18n_yaml_editor/transformation'
|
15
|
+
require 'i18n_yaml_editor/translation'
|
16
|
+
require 'i18n_yaml_editor/web'
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$VERBOSE = nil
|
4
|
+
|
5
|
+
require 'simplecov'
|
6
|
+
SimpleCov.start do
|
7
|
+
add_filter '/test'
|
8
|
+
end
|
9
|
+
|
10
|
+
require 'minitest/autorun'
|
11
|
+
require 'minitest/hell'
|
12
|
+
require 'minitest/reporters'
|
13
|
+
require 'rack/test'
|
14
|
+
require 'i18n_yaml_editor'
|
15
|
+
|
16
|
+
Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)]
|
17
|
+
|
18
|
+
module Minitest
|
19
|
+
class Test
|
20
|
+
include I18nYamlEditor
|
21
|
+
parallelize_me!
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'i18n_yaml_editor/app'
|
5
|
+
|
6
|
+
class TestApp < Minitest::Test
|
7
|
+
def test_save_translations
|
8
|
+
Dir.mktmpdir do |dir|
|
9
|
+
FileUtils.copy './example/session.en.yml', dir
|
10
|
+
@app = App.new(dir)
|
11
|
+
@app.load_translations
|
12
|
+
@app.save_translations('en.session.create.logged_in' => 'Test123')
|
13
|
+
assert_match(/logged_in: Test123\n/, File.read(dir + '/session.en.yml'))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_save_translations_empty
|
18
|
+
Dir.mktmpdir do |dir|
|
19
|
+
FileUtils.copy './example/session.en.yml', dir
|
20
|
+
@app = App.new(dir)
|
21
|
+
@app.load_translations
|
22
|
+
assert_output('', "Translation not found\n") do
|
23
|
+
@app.save_translations('does.not.exist' => 'foo')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_save_translations_array
|
29
|
+
Dir.mktmpdir do |dir|
|
30
|
+
FileUtils.copy './example/da.yml', dir
|
31
|
+
@app = App.new(dir)
|
32
|
+
@app.load_translations
|
33
|
+
@app.save_translations('da.day_names' => "Test1\r\nTest2")
|
34
|
+
assert_match(/- Test2\n/, File.read(dir + '/da.yml'))
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_save_translations_boolean
|
39
|
+
Dir.mktmpdir do |dir|
|
40
|
+
FileUtils.copy './example/session.en.yml', dir
|
41
|
+
@app = App.new(dir)
|
42
|
+
@app.load_translations
|
43
|
+
@app.save_translations('en.numbers.use_local_format' => 'false')
|
44
|
+
assert_match(/use_local_format: false\n/,
|
45
|
+
File.read(dir + '/session.en.yml'))
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_save_translations_number
|
50
|
+
Dir.mktmpdir do |dir|
|
51
|
+
FileUtils.copy './example/session.en.yml', dir
|
52
|
+
@app = App.new(dir)
|
53
|
+
@app.load_translations
|
54
|
+
@app.save_translations('en.numbers.precision' => '10')
|
55
|
+
assert_match(/precision: 10\n/, File.read(dir + '/session.en.yml'))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'i18n_yaml_editor/category'
|
5
|
+
|
6
|
+
class TestCategory < Minitest::Test
|
7
|
+
def setup
|
8
|
+
@category = Category.new(name: 'test')
|
9
|
+
@key = Minitest::Mock.new([0])
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_init_name
|
13
|
+
assert_equal('test', @category.name)
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_init_keys
|
17
|
+
assert_empty(@category.keys)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_add_key
|
21
|
+
@category.add_key(@key)
|
22
|
+
assert_includes(@category.keys, @key)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_is_complete
|
26
|
+
@category.add_key(@key)
|
27
|
+
@key.expect :complete?, true
|
28
|
+
assert @category.complete?
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_is_not_complete
|
32
|
+
@key.expect :complete?, true
|
33
|
+
@category.add_key(@key)
|
34
|
+
|
35
|
+
@key2 = Minitest::Mock.new([1])
|
36
|
+
@key2.expect :complete?, false
|
37
|
+
@category.add_key(@key2)
|
38
|
+
|
39
|
+
assert !@category.complete?
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'i18n_yaml_editor/key'
|
5
|
+
|
6
|
+
class TestKey < Minitest::Test
|
7
|
+
def test_category
|
8
|
+
key = Key.new(name: 'session.login')
|
9
|
+
assert_equal 'session', key.category
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_complete_with_text_for_all_translations
|
13
|
+
key = Key.new(name: 'session.login')
|
14
|
+
key.add_translation Translation.new(name: 'da.session.login',
|
15
|
+
text: 'Log ind')
|
16
|
+
key.add_translation Translation.new(name: 'en.session.login',
|
17
|
+
text: 'Sign in')
|
18
|
+
|
19
|
+
assert key.complete?
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_complete_with_no_texts
|
23
|
+
key = Key.new(name: 'session.login')
|
24
|
+
key.add_translation Translation.new(name: 'da.session.login')
|
25
|
+
key.add_translation Translation.new(name: 'en.session.login')
|
26
|
+
|
27
|
+
assert key.complete?
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_not_complete_without_text_for_some_translations
|
31
|
+
key = Key.new(name: 'session.login')
|
32
|
+
key.add_translation Translation.new(name: 'da.session.login',
|
33
|
+
text: 'Log ind')
|
34
|
+
key.add_translation Translation.new(name: 'en.session.login')
|
35
|
+
|
36
|
+
assert_equal false, key.complete?
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_emptycomplete_with_no_texts
|
40
|
+
key = Key.new(name: 'session.login')
|
41
|
+
key.add_translation Translation.new(name: 'da.session.login')
|
42
|
+
key.add_translation Translation.new(name: 'en.session.login')
|
43
|
+
|
44
|
+
assert key.empty?
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'unit/test_store_data'
|
5
|
+
require 'i18n_yaml_editor/store'
|
6
|
+
|
7
|
+
class TestStore < Minitest::Test
|
8
|
+
include TestStoreData
|
9
|
+
|
10
|
+
attr_accessor :input, :expected_yaml
|
11
|
+
|
12
|
+
TEST_DATA = {
|
13
|
+
'/tmp/session.da.yml' => {
|
14
|
+
da: { session: { login: 'Log ind', logout: 'Log ud' } }
|
15
|
+
},
|
16
|
+
'/tmp/session.en.yml' => {
|
17
|
+
en: { session: { login: 'Sign in' } }
|
18
|
+
},
|
19
|
+
'/tmp/da.yml' => {
|
20
|
+
da: { app_name: 'Oversætter' }
|
21
|
+
}
|
22
|
+
}.freeze
|
23
|
+
|
24
|
+
def initialize(*)
|
25
|
+
self.input = {
|
26
|
+
da: { session: { login: 'Log ind' } }
|
27
|
+
}
|
28
|
+
|
29
|
+
self.expected_yaml = TEST_DATA.with_indifferent_access
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_add_translations_store_translations
|
34
|
+
store = Store.new
|
35
|
+
translation = Translation.new(name: 'da.session.login')
|
36
|
+
|
37
|
+
store.add_translation(translation)
|
38
|
+
|
39
|
+
assert_equal 1, store.translations.size
|
40
|
+
assert_equal translation, store.translations[translation.name]
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_add_translations_store_keys
|
44
|
+
store = Store.new
|
45
|
+
translation = Translation.new(name: 'da.session.login')
|
46
|
+
|
47
|
+
store.add_translation(translation)
|
48
|
+
|
49
|
+
assert_equal 1, store.keys.size
|
50
|
+
assert_equal [translation].to_set, store.keys['session.login'].translations
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_add_translations_store_categories
|
54
|
+
store = Store.new
|
55
|
+
translation = Translation.new(name: 'da.session.login')
|
56
|
+
|
57
|
+
store.add_translation(translation)
|
58
|
+
|
59
|
+
assert_equal 1, store.categories.size
|
60
|
+
assert_equal %w[session.login], store.categories['session'].keys.map(&:name)
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_add_translations_store_locales
|
64
|
+
store = Store.new
|
65
|
+
translation = Translation.new(name: 'da.session.login')
|
66
|
+
|
67
|
+
store.add_translation(translation)
|
68
|
+
|
69
|
+
assert_equal 1, store.locales.size
|
70
|
+
assert_equal %w[da], store.locales.to_a
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_add_duplicate_translation
|
74
|
+
store = Store.new
|
75
|
+
t1 = Translation.new(name: 'da.session.login')
|
76
|
+
t2 = Translation.new(name: 'da.session.login')
|
77
|
+
store.add_translation(t1)
|
78
|
+
|
79
|
+
assert_raises(DuplicateTranslationError) do
|
80
|
+
store.add_translation(t2)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_create_missing_translations
|
85
|
+
store = init_test_store_0
|
86
|
+
|
87
|
+
store.create_missing_keys
|
88
|
+
|
89
|
+
assert(translation = store.translations['en.session.login'])
|
90
|
+
assert_equal 'en.session.login', translation.name
|
91
|
+
assert_equal '/tmp/session.en.yml', translation.file
|
92
|
+
assert_nil translation.text
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_create_missing_translations_in_top_level_file
|
96
|
+
store = init_test_store_1
|
97
|
+
|
98
|
+
store.create_missing_keys
|
99
|
+
|
100
|
+
assert(translation = store.translations['en.app_name'])
|
101
|
+
assert_equal 'en.app_name', translation.name
|
102
|
+
assert_equal '/tmp/en.yml', translation.file
|
103
|
+
assert_nil translation.text
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_from_yaml
|
107
|
+
store = Store.new
|
108
|
+
|
109
|
+
store.from_yaml(input)
|
110
|
+
|
111
|
+
assert_equal 1, store.translations.size
|
112
|
+
translation = store.translations['da.session.login']
|
113
|
+
assert_equal 'da.session.login', translation.name
|
114
|
+
assert_equal 'Log ind', translation.text
|
115
|
+
end
|
116
|
+
|
117
|
+
def test_to_yaml
|
118
|
+
store = Store.new
|
119
|
+
[{ name: 'da.session.login', text: 'Log ind', file: '/tmp/session.da.yml' },
|
120
|
+
{ name: 'en.session.login', text: 'Sign in', file: '/tmp/session.en.yml' },
|
121
|
+
{ name: 'da.session.logout', text: 'Log ud', file: '/tmp/session.da.yml' },
|
122
|
+
{ name: 'da.app_name', text: 'Oversætter',
|
123
|
+
file: '/tmp/da.yml' }].each do |translation|
|
124
|
+
store.add_translation Translation.new(translation)
|
125
|
+
end
|
126
|
+
|
127
|
+
assert_equal @expected_yaml, store.to_yaml
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'i18n_yaml_editor/transformation'
|
5
|
+
|
6
|
+
class TestTransformation < Minitest::Test
|
7
|
+
I18N_HASH = {
|
8
|
+
'da.session.login' => 'Log ind',
|
9
|
+
'da.session.logout' => 'Log ud',
|
10
|
+
'en.session.login' => 'Log in',
|
11
|
+
'en.session.logout' => 'Log out'
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
def test_flatten_hash
|
15
|
+
input = {
|
16
|
+
da: {
|
17
|
+
session: { login: 'Log ind', logout: 'Log ud' }
|
18
|
+
},
|
19
|
+
en: {
|
20
|
+
session: { login: 'Log in', logout: 'Log out' }
|
21
|
+
}
|
22
|
+
}
|
23
|
+
|
24
|
+
assert_equal I18N_HASH, Transformation.flatten_hash(input)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_nest_hash
|
28
|
+
expected = {
|
29
|
+
da: {
|
30
|
+
session: { login: 'Log ind', logout: 'Log ud' }
|
31
|
+
},
|
32
|
+
en: {
|
33
|
+
session: { login: 'Log in', logout: 'Log out' }
|
34
|
+
}
|
35
|
+
}.with_indifferent_access
|
36
|
+
|
37
|
+
assert_equal expected, Transformation.nest_hash(I18N_HASH)
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_nest_hash_transformation_error
|
41
|
+
assert_raises(TransformationError) do
|
42
|
+
Transformation.nest_hash(error: 'value')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'i18n_yaml_editor/translation'
|
5
|
+
|
6
|
+
class TestTranslation < Minitest::Test
|
7
|
+
def test_key
|
8
|
+
translation = Translation.new(name: 'da.session.login')
|
9
|
+
assert_equal 'session.login', translation.key
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_locale
|
13
|
+
translation = Translation.new(name: 'da.session.login')
|
14
|
+
assert_equal 'da', translation.locale
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<table class="categories" cellspacing="0">
|
2
|
+
<% categories.each do |name, category| %>
|
3
|
+
<tr class="category <%= "incomplete" unless category.complete? %>">
|
4
|
+
<td class="key">
|
5
|
+
<a class="text" href="/?filters[key]=<%= Rack::Utils.escape("^#{category.name}") %>"><%= category.name %></a>
|
6
|
+
</td>
|
7
|
+
</tr>
|
8
|
+
<% end %>
|
9
|
+
</table>
|