i18n_yaml_editor 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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'
@@ -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>
@@ -0,0 +1,9 @@
1
+ <html>
2
+ <body>
3
+ <pre>
4
+ <% translations.each do |translation| %>
5
+ <%= Rack::Utils.escape_html(translation.inspect) %>
6
+ <% end %>
7
+ </pre>
8
+ </body>
9
+ </html>