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
@@ -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>
|