releaf-i18n_database 0.2.1

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.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +24 -0
  3. data/app/assets/javascripts/releaf/controllers/releaf/i18n_database/translations.js +22 -0
  4. data/app/assets/stylesheets/releaf/controllers/releaf/i18n_database/translations.scss +95 -0
  5. data/app/builders/releaf/i18n_database/translations/builders_common.rb +13 -0
  6. data/app/builders/releaf/i18n_database/translations/edit_builder.rb +41 -0
  7. data/app/builders/releaf/i18n_database/translations/index_builder.rb +44 -0
  8. data/app/builders/releaf/i18n_database/translations/table_builder.rb +31 -0
  9. data/app/controllers/releaf/i18n_database/translations_controller.rb +171 -0
  10. data/app/lib/releaf/i18n_database/translations_importer.rb +72 -0
  11. data/app/lib/releaf/i18n_database/translations_utilities.rb +66 -0
  12. data/app/models/releaf/i18n_database/translation.rb +17 -0
  13. data/app/models/releaf/i18n_database/translation_data.rb +11 -0
  14. data/app/views/releaf/i18n_database/translations/_form_fields.haml +40 -0
  15. data/app/views/releaf/i18n_database/translations/export.xlsx.axlsx +24 -0
  16. data/lib/releaf-i18n_database.rb +5 -0
  17. data/lib/releaf/i18n_database/backend.rb +169 -0
  18. data/lib/releaf/i18n_database/builders_autoload.rb +10 -0
  19. data/lib/releaf/i18n_database/engine.rb +36 -0
  20. data/lib/releaf/i18n_database/humanize_missing_translations.rb +15 -0
  21. data/releaf-i18n_database.gemspec +21 -0
  22. data/spec/builders/translations/builder_common_spec.rb +39 -0
  23. data/spec/builders/translations/edit_builder_spec.rb +93 -0
  24. data/spec/builders/translations/index_builder_spec.rb +96 -0
  25. data/spec/builders/translations/table_builder_spec.rb +68 -0
  26. data/spec/controllers/i18n_backend/translations_controller_spec.rb +157 -0
  27. data/spec/features/translations_spec.rb +162 -0
  28. data/spec/fixtures/all_translations_exported.xlsx +0 -0
  29. data/spec/fixtures/time.formats.xlsx +0 -0
  30. data/spec/fixtures/translations_import.xlsx +0 -0
  31. data/spec/fixtures/unsupported_import_file.png +0 -0
  32. data/spec/lib/i18n_database/backend_spec.rb +337 -0
  33. data/spec/lib/i18n_database/humanize_missing_translations_spec.rb +18 -0
  34. data/spec/lib/i18n_database/translations_importer_spec.rb +17 -0
  35. data/spec/lib/i18n_database/translations_utilities_spec.rb +175 -0
  36. data/spec/models/i18n_database/translation_data_spec.rb +13 -0
  37. data/spec/models/i18n_database/translation_spec.rb +49 -0
  38. metadata +151 -0
@@ -0,0 +1,15 @@
1
+ module Releaf::I18nDatabase
2
+ module HumanizeMissingTranslations
3
+ def call(exception, locale, key, options)
4
+ if exception.is_a?(I18n::MissingTranslation)
5
+ key.to_s.split('.').last.humanize
6
+ else
7
+ super
8
+ end
9
+ end
10
+
11
+ def self.initialize_component
12
+ I18n.exception_handler.extend(self)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ require File.expand_path("../../releaf-core/lib/releaf/version.rb", __FILE__)
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "releaf-i18n_database"
5
+ s.version = Releaf::VERSION
6
+
7
+ s.summary = "i18n database gem for releaf"
8
+ s.description = "i18n database backend for releaf"
9
+ s.authors = ["CubeSystems"]
10
+ s.email = 'info@cubesystems.lv'
11
+ s.homepage = 'https://github.com/cubesystems/releaf'
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.test_files = Dir["spec/**/*"]
15
+
16
+ s.add_dependency 'releaf-core', Releaf::VERSION
17
+ s.add_dependency 'twitter_cldr'
18
+ s.add_dependency 'axlsx_rails', '>= 0.3.0'
19
+ s.add_dependency 'roo'
20
+
21
+ end
@@ -0,0 +1,39 @@
1
+ require "rails_helper"
2
+
3
+ describe Releaf::I18nDatabase::Translations::BuildersCommon, type: :class do
4
+ class I18nBuildersCommonInheriter < Releaf::Builders::IndexBuilder
5
+ include Releaf::I18nDatabase::Translations::BuildersCommon
6
+ end
7
+ class TableBuilderTestHelper < ActionView::Base; end
8
+ let(:template){ TableBuilderTestHelper.new }
9
+ let(:subject){ I18nBuildersCommonInheriter.new(template) }
10
+
11
+ describe "#action_url" do
12
+ before do
13
+ request = ActionDispatch::Request.new("X")
14
+ allow(request).to receive(:query_parameters).and_return(a: "b", c: "d")
15
+ allow(subject).to receive(:request).and_return(request)
16
+ end
17
+
18
+ it "returns url for given action with current query params" do
19
+ allow(subject).to receive(:url_for).with(a: "b", c: "d", action: :edit).and_return("url")
20
+ expect(subject.action_url(:edit)).to eq("url")
21
+ end
22
+
23
+ context "when extra params given" do
24
+ it "merges given params to url" do
25
+ allow(subject).to receive(:url_for).with(a: "b", c: "z", action: :edit, format: "xx").and_return("url")
26
+ expect(subject.action_url(:edit, format: "xx", c: "z")).to eq("url")
27
+ end
28
+ end
29
+ end
30
+
31
+ describe "#export_button" do
32
+ it "returns export button" do
33
+ allow(subject).to receive(:t).with("export").and_return("exp")
34
+ allow(subject).to receive(:action_url).with(:export, format: :xlsx).and_return("_exp_url_")
35
+ allow(subject).to receive(:button).with("exp", "download", class: "secondary", href: "_exp_url_").and_return("btn")
36
+ expect(subject.export_button).to eq('btn')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,93 @@
1
+ require "rails_helper"
2
+
3
+ describe Releaf::I18nDatabase::Translations::EditBuilder, type: :class do
4
+ class TableBuilderTestHelper < ActionView::Base
5
+ def protect_against_forgery?; end
6
+ end
7
+ let(:template){ TableBuilderTestHelper.new }
8
+ let(:resource_class){ Releaf::I18nDatabase::Translation }
9
+ let(:subject){ described_class.new(template) }
10
+
11
+ describe "#section" do
12
+ it "returns section blocks wrapped within edit form" do
13
+ allow(subject).to receive(:action_url).with(:update).and_return("update_url")
14
+ allow(subject).to receive(:section_blocks).and_return(["a", "b"])
15
+ result = '<section><form action="update_url" accept-charset="UTF-8" method="post"><input name="utf8" type="hidden" value="&#x2713;" />ab</form></section>'
16
+ expect(subject.section).to eq(result)
17
+ end
18
+ end
19
+
20
+ describe "#section_body" do
21
+ it "returns section body with form fields partial" do
22
+ allow(subject).to receive(:render).with(partial: "form_fields", locals: {builder: subject}).and_return("xxx")
23
+ expect(subject.section_body).to eq('<div class="body">xxx</div>')
24
+ end
25
+ end
26
+
27
+ describe "#import?" do
28
+ it "returns template `import` value" do
29
+ allow(subject).to receive(:template_variable).with("import").and_return("x")
30
+ expect(subject.import?).to eq("x")
31
+ end
32
+ end
33
+
34
+ describe "#save_button" do
35
+ it "returns localized value for given resource and column(locale)" do
36
+ allow(subject).to receive(:save_button_text).and_return("sv_txt")
37
+ allow(subject).to receive(:button).with("sv_txt", "check", class: "primary", data: { type: 'ok' }, type: "submit").and_return("save_btn")
38
+ expect(subject.save_button).to eq("save_btn")
39
+ end
40
+ end
41
+
42
+ describe "#save_button_text" do
43
+ context "when within import view" do
44
+ it "returns translated `Import` text" do
45
+ allow(subject).to receive(:t).with("Import").and_return("_import_")
46
+ allow(subject).to receive(:import?).and_return(true)
47
+ expect(subject.save_button_text).to eq("_import_")
48
+ end
49
+ end
50
+
51
+ context "when not within import view" do
52
+ it "returns translated `Save` text" do
53
+ allow(subject).to receive(:t).with("Save").and_return("_save_")
54
+ allow(subject).to receive(:import?).and_return(false)
55
+ expect(subject.save_button_text).to eq("_save_")
56
+ end
57
+ end
58
+ end
59
+
60
+ describe "#footer_secondary_tools" do
61
+ before do
62
+ allow(subject).to receive(:back_to_index_button).and_return("indx_btn")
63
+ allow(subject).to receive(:export_button).and_return("xprt_btn")
64
+ end
65
+
66
+ it "returns array with back and export links" do
67
+ allow(subject).to receive(:import?).and_return(false)
68
+ expect(subject.footer_secondary_tools).to eq(["indx_btn", "xprt_btn"])
69
+ end
70
+
71
+ context "when within import view" do
72
+ it "does not incliude export button" do
73
+ allow(subject).to receive(:import?).and_return(true)
74
+ expect(subject.footer_secondary_tools).to eq(["indx_btn"])
75
+ end
76
+ end
77
+ end
78
+
79
+ describe "#back_to_index_button" do
80
+ it "returns localized value for given resource and column(locale)" do
81
+ allow(subject).to receive(:t).with("Back to list").and_return("back")
82
+ allow(subject).to receive(:action_url).with(:index).and_return("index_url")
83
+ allow(subject).to receive(:button).with("back", "caret-left", class: "secondary", href: "index_url").and_return("index_btn")
84
+ expect(subject.back_to_index_button).to eq("index_btn")
85
+ end
86
+ end
87
+
88
+ describe "#section_header" do
89
+ it "returns nil" do
90
+ expect(subject.section_header).to be nil
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,96 @@
1
+ require "rails_helper"
2
+
3
+ describe Releaf::I18nDatabase::Translations::IndexBuilder, type: :class do
4
+ class IndexBuilderTestHelper < ActionView::Base
5
+ include Releaf::ApplicationHelper
6
+ delegate :resource_class, :table_options, to: :controller
7
+
8
+ def protect_against_forgery?; end
9
+
10
+ def controller
11
+ @controller ||= begin
12
+ c = Releaf::I18nDatabase::TranslationsController.new
13
+ c.setup
14
+ c
15
+ end
16
+ end
17
+ end
18
+ let(:template){ IndexBuilderTestHelper.new }
19
+ let(:resource_class){ Releaf::I18nDatabase::Translation }
20
+ let(:subject){ described_class.new(template) }
21
+
22
+ describe "#text_search_content" do
23
+ it "adds blank translation checbox to text search" do
24
+ allow(subject).to receive(:t).and_return("search")
25
+ allow(subject).to receive(:params).and_return(search: "xx")
26
+ allow(subject).to receive(:button).and_return("btn")
27
+ allow(subject).to receive(:search_only_blank_ui).and_return("_blank_ui_")
28
+ expect(subject.text_search_content).to start_with("_blank_ui_")
29
+ end
30
+ end
31
+
32
+ describe "#search_only_blank_ui" do
33
+ before do
34
+ allow(subject).to receive(:t).with("Only blank").and_return("trnls")
35
+ end
36
+
37
+ it "returns only blank translation search ui" do
38
+ allow(subject).to receive(:params).and_return(search: "xx")
39
+ expect(subject.search_only_blank_ui).to match_html(%Q[
40
+ <div class="search-field" data-name="only-blank">
41
+ <input type="checkbox" name="only_blank" id="only_blank" value="true" />
42
+ <label for="only_blank">trnls</label>
43
+ </div>
44
+ ])
45
+ end
46
+
47
+ it "reflects `only_blank` params to checkbox state" do
48
+ allow(subject).to receive(:params).and_return(only_blank: "1", search: true)
49
+ expect(subject.search_only_blank_ui).to match_html(%Q[
50
+ <div class="search-field" data-name="only-blank">
51
+ <input type="checkbox" name="only_blank" id="only_blank" value="true" checked="checked" />
52
+ <label for=\"only_blank\">trnls</label>
53
+ </div>
54
+ ])
55
+ end
56
+ end
57
+
58
+ describe "#footer_primary_tools" do
59
+ it "returns array with edit button" do
60
+ allow(subject).to receive(:edit_button).and_return("btn")
61
+ expect(subject.footer_primary_tools).to eq(["btn"])
62
+ end
63
+ end
64
+
65
+ describe "#footer_secondary_tools" do
66
+ it "returns array with edit button" do
67
+ allow(subject).to receive(:export_button).and_return("a")
68
+ allow(subject).to receive(:import_button).and_return("b")
69
+ allow(subject).to receive(:import_form).and_return("c")
70
+ expect(subject.footer_secondary_tools).to eq(["a", "b", "c"])
71
+ end
72
+ end
73
+
74
+ describe "#import_form" do
75
+ it "returns import form" do
76
+ allow(subject).to receive(:url_for).with(action: 'import').and_return("import_url")
77
+ result = "<form class=\"import\" enctype=\"multipart/form-data\" action=\"import_url\" accept-charset=\"UTF-8\" method=\"post\"><input name=\"utf8\" type=\"hidden\" value=\"&#x2713;\" /><input type=\"file\" name=\"import_file\" id=\"import_file\" /></form>"
78
+ expect(subject.import_form).to eq(result)
79
+ end
80
+ end
81
+
82
+ describe "#edit_button" do
83
+ it "return edit button" do
84
+ allow(subject).to receive(:t).with("Edit").and_return("edt")
85
+ allow(subject).to receive(:action_url).with(:edit).and_return("edt_url")
86
+ allow(subject).to receive(:button).with("edt", "edit", class: "primary", href: "edt_url").and_return("edt_btn")
87
+ expect(subject.edit_button).to eq("edt_btn")
88
+ end
89
+ end
90
+
91
+ describe "#text_search_available?" do
92
+ it "return true" do
93
+ expect(subject.text_search_available?).to be true
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,68 @@
1
+ require "rails_helper"
2
+
3
+ describe Releaf::I18nDatabase::Translations::TableBuilder, type: :class do
4
+ class TableBuilderTestHelper < ActionView::Base; end
5
+ let(:template){ TableBuilderTestHelper.new }
6
+ let(:resource_class){ Releaf::I18nDatabase::Translation }
7
+ let(:subject){ described_class.new([], resource_class, template, {}) }
8
+
9
+ before do
10
+ allow(Releaf.application.config).to receive(:all_locales).and_return(["de", "ze"])
11
+ end
12
+
13
+ describe "#column_names" do
14
+ it "returns key and Releaf.application.config column names array" do
15
+ expect(subject.column_names).to eq([:key, "de", "ze"])
16
+ end
17
+ end
18
+
19
+ describe "#head_cell_content" do
20
+ context "when locale column given" do
21
+ it "returns head cell content with translated locale" do
22
+ allow(subject).to receive(:translate_locale).with("de").and_return("gegxxxeg")
23
+ expect(subject.head_cell_content("de")).to eq('gegxxxeg')
24
+ end
25
+ end
26
+
27
+ context "when non locale column given" do
28
+ it "returns head cell content with translated locale" do
29
+ expect(subject).to_not receive(:translate_locale)
30
+ expect(subject.head_cell_content("lv")).to eq('Lv')
31
+ end
32
+ end
33
+ end
34
+
35
+ describe "#cell_content" do
36
+ it "wraps content within span element" do
37
+ resource = resource_class.new(key: "xx")
38
+ expect(subject.cell_content(resource, :key, format_method: :format_string_content)).to eq("<span>xx</span>")
39
+ end
40
+ end
41
+
42
+ describe "#locale_value" do
43
+ it "returns localized value for given resource and column(locale)" do
44
+ resource = resource_class.new
45
+ allow(resource).to receive(:locale_value).with(:en).and_return("en value")
46
+ expect(subject.locale_value(resource, :en)).to eq("en value")
47
+ end
48
+ end
49
+
50
+ describe "#cell_format_method" do
51
+ before do
52
+ allow(Releaf.application.config).to receive(:all_locales).and_return([:de, :ze])
53
+ end
54
+
55
+ context "when given column name exists within Releaf.application.config.all_locales" do
56
+ it "returns :locale_value" do
57
+ expect(subject.cell_format_method(:de)).to eq(:locale_value)
58
+ expect(subject.cell_format_method(:ze)).to eq(:locale_value)
59
+ end
60
+ end
61
+
62
+ context "when given column name does not exists within Releaf.application.config.all_locales" do
63
+ it "return super" do
64
+ expect(subject.cell_format_method(:en)).to eq(:format_string_content)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,157 @@
1
+ require 'rails_helper'
2
+
3
+ describe Releaf::I18nDatabase::TranslationsController do
4
+ def file_attachment
5
+ test_document = File.expand_path('../../fixtures/translations_import.xlsx', __dir__)
6
+ Rack::Test::UploadedFile.new(test_document)
7
+ end
8
+
9
+ login_as_user :user
10
+
11
+ before do
12
+ @time_now = Time.parse("1981-02-23 21:00:00 UTC")
13
+ allow(Time).to receive(:now).and_return(@time_now)
14
+ end
15
+
16
+ before build_translations: true do
17
+ @t1 = FactoryGirl.create(:translation, key: 'test.key1')
18
+ @t2 = FactoryGirl.create(:translation, key: 'great.stuff')
19
+ @t3 = FactoryGirl.create(:translation, key: 'geek.stuff')
20
+
21
+ @t1_en = FactoryGirl.create(:translation_data, lang: 'en', localization: 'testa atslēga', translation_id: @t1.id)
22
+
23
+ @t2_en = FactoryGirl.create(:translation_data, lang: 'en', localization: 'awesome stuff', translation_id: @t2.id)
24
+ @t2_lv = FactoryGirl.create(:translation_data, lang: 'lv', localization: 'lieliska manta', translation_id: @t2.id)
25
+
26
+ @t3_en = FactoryGirl.create(:translation_data, lang: 'en', localization: 'geek stuff', translation_id: @t3.id)
27
+ @t3_lv = FactoryGirl.create(:translation_data, lang: 'lv', localization: 'nūģu lieta', translation_id: @t3.id)
28
+ end
29
+
30
+
31
+ describe "GET #index", build_translations: true do
32
+ context "when not searching" do
33
+ it "renders all translations" do
34
+ get :index
35
+ expect( assigns(:collection).size ).to eq(3)
36
+ end
37
+ end
38
+
39
+ context "when searching" do
40
+ it "searches by translation key" do
41
+ get :index, search: 'great'
42
+ expect( assigns(:collection).size ).to eq(1)
43
+ end
44
+
45
+ it "searched by localized values" do
46
+ get :index, search: 'manta'
47
+ expect( assigns(:collection).size ).to eq(1)
48
+ end
49
+ end
50
+
51
+ context "when searching blank translations" do
52
+ it "returns translations that has blank translation in any localization" do
53
+ get :index, only_blank: 'true'
54
+ expect( assigns(:collection).map(&:id) ).to match_array [@t1.id]
55
+ end
56
+ end
57
+ end
58
+
59
+ describe "GET #edit", build_translations: true do
60
+ context "when search scope is not given" do
61
+ it "renders all translations" do
62
+ get :edit
63
+ expect( assigns(:collection).size ).to eq(3)
64
+ end
65
+ end
66
+
67
+ context "when search scope is given" do
68
+ it "renders translations matching search pattern" do
69
+ get :index, search: 'stuff'
70
+ expect( assigns(:collection).size ).to eq(2)
71
+ end
72
+ end
73
+ end
74
+
75
+ describe "#update" do
76
+ context "when save successful" do
77
+ it "updates translations updated_at" do
78
+ expect(Releaf::I18nDatabase::Backend).to receive("translations_updated_at=").with(@time_now)
79
+ put :update, translations: [{key: 'a.b.c', localizations: {en: 'test', lv: 'xxl'}}]
80
+ end
81
+
82
+ context "when save with import" do
83
+ before do
84
+ put :update, translations: [{key: 'a.b.c', localizations: {en: 'test', lv: 'xxl'}}], import: "true"
85
+ end
86
+
87
+ it "redirects to index view" do
88
+ expect(subject).to redirect_to(action: :index)
89
+ end
90
+
91
+ it "flash success notification with updated count" do
92
+ expect(flash["success"]).to eq("id" => "resource_status", "message" => "successfuly imported 1 translations")
93
+ end
94
+ end
95
+
96
+ context "when save without import" do
97
+ before do
98
+ put :update, translations: [{key: 'a.b.c', localizations: {en: 'test', lv: 'xxl'}}]
99
+ end
100
+
101
+ it "redirects to edit view" do
102
+ expect(subject).to redirect_to(action: :edit)
103
+ end
104
+
105
+ it "flash success notification" do
106
+ expect(flash["success"]).to eq("id" => "resource_status", "message" => "Update succeeded")
107
+ end
108
+ end
109
+ end
110
+
111
+ context "when save failed" do
112
+ it "renders edit view" do
113
+ put :update, translations: [{key: '', localizations: {en: 'test', lv: 'xxl'}}]
114
+ expect(response).to render_template(:edit)
115
+ end
116
+
117
+ it "flash error notification" do
118
+ put :update, translations: [{key: '', localizations: {en: 'test', lv: 'xxl'}}]
119
+ expect(flash["error"]).to eq("id" => "resource_status", "message" => "Update failed")
120
+ end
121
+ end
122
+ end
123
+
124
+ describe "#import" do
125
+ context "when file uploaded" do
126
+ before do
127
+ file = fixture_file_upload(File.expand_path('../../fixtures/translations_import.xlsx', __dir__), 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
128
+ class << file
129
+ # The reader method is present in a real invocation,
130
+ # but missing from the fixture object for some reason (Rails 3.1.1)
131
+ attr_reader :tempfile
132
+ end
133
+
134
+ post :import, import_file: file
135
+ end
136
+
137
+ it "parses uploaded file and assigns content to collection" do
138
+ expect( assigns(:collection).size ).to eq(4)
139
+ end
140
+
141
+ it "assigns @import to true" do
142
+ expect( assigns(:import) ).to be true
143
+ end
144
+
145
+ it "appends breadcrumb with 'import' part" do
146
+ expect( assigns(:breadcrumbs).last ).to eq({name: "Import"})
147
+ end
148
+ end
149
+
150
+ context "when no file uploaded" do
151
+ it "redirects to index" do
152
+ post :import
153
+ expect(subject).to redirect_to(action: :index)
154
+ end
155
+ end
156
+ end
157
+ end