releaf-i18n_database 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
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