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