lolita-translation 0.3.3 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. data/.gitignore +25 -0
  2. data/Gemfile +2 -18
  3. data/README.md +1 -2
  4. data/Rakefile +1 -49
  5. data/app/assets/javascripts/lolita/translation/application.js +4 -3
  6. data/app/assets/stylesheets/lolita/translation/application.css +7 -1
  7. data/app/views/components/lolita/translation/_assets.html.haml +4 -0
  8. data/app/views/components/lolita/translation/_language_wrap.html.haml +2 -0
  9. data/app/views/components/lolita/translation/_switch.html.haml +4 -0
  10. data/lib/lolita-translation.rb +40 -33
  11. data/lib/lolita-translation/builder/abstract_builder.rb +103 -0
  12. data/lib/lolita-translation/builder/active_record_builder.rb +106 -0
  13. data/lib/lolita-translation/builder/mongoid_builder.rb +0 -0
  14. data/lib/lolita-translation/configuration.rb +41 -166
  15. data/lib/lolita-translation/errors.rb +15 -0
  16. data/lib/lolita-translation/locale.rb +32 -0
  17. data/lib/lolita-translation/locales.rb +62 -0
  18. data/lib/lolita-translation/lolita/component_hooks.rb +49 -0
  19. data/lib/lolita-translation/lolita/tab_extension.rb +113 -0
  20. data/lib/lolita-translation/migrator.rb +56 -0
  21. data/lib/lolita-translation/migrators/active_record_migrator.rb +93 -0
  22. data/lib/lolita-translation/migrators/mongoid_migrator.rb +81 -0
  23. data/lib/lolita-translation/orm/mixin.rb +57 -0
  24. data/lib/lolita-translation/rails.rb +6 -31
  25. data/lib/lolita-translation/record.rb +220 -0
  26. data/lib/lolita-translation/translated_string.rb +11 -0
  27. data/lib/lolita-translation/translation_class_builder.rb +59 -0
  28. data/lib/lolita-translation/utils.rb +13 -0
  29. data/lib/lolita-translation/version.rb +32 -0
  30. data/lib/tasks/lolita_translation.rake +14 -0
  31. data/lolita-translation.gemspec +24 -71
  32. data/spec/ar_schema.rb +90 -0
  33. data/spec/header.rb +14 -0
  34. data/spec/integrations/active_record_integration_spec.rb +218 -0
  35. data/spec/lolita-translation/builder/abstract_builder_spec.rb +67 -0
  36. data/spec/lolita-translation/builder/active_record_builder_spec.rb +40 -0
  37. data/spec/lolita-translation/configuration_spec.rb +72 -0
  38. data/spec/lolita-translation/locale_spec.rb +25 -0
  39. data/spec/lolita-translation/locales_spec.rb +31 -0
  40. data/spec/lolita-translation/lolita/tab_extension_spec.rb +61 -0
  41. data/spec/lolita-translation/migrator_spec.rb +42 -0
  42. data/spec/lolita-translation/migrators/active_record_migrator_spec.rb +50 -0
  43. data/spec/lolita-translation/orm/mixin_spec.rb +52 -0
  44. data/spec/lolita-translation/record_spec.rb +112 -0
  45. data/spec/lolita-translation/translation_class_builder_spec.rb +62 -0
  46. data/spec/lolita_translation_spec.rb +16 -0
  47. data/spec/rails_helper.rb +6 -0
  48. data/spec/requests/record_language_switch_spec.rb +16 -0
  49. data/spec/requests/record_saving_spec.rb +63 -0
  50. data/spec/spec_helper.rb +38 -90
  51. data/spec/tasks/lolita_translation_spec.rb +32 -0
  52. data/spec/test_app/app/controllers/application_controller.rb +3 -0
  53. data/spec/test_app/app/models/category.rb +6 -0
  54. data/spec/test_app/app/models/post.rb +8 -0
  55. data/spec/test_app/config/application.rb +24 -0
  56. data/spec/test_app/config/boot.rb +11 -0
  57. data/spec/test_app/config/database.yml +3 -0
  58. data/spec/test_app/config/enviroment.rb +5 -0
  59. data/spec/test_app/config/enviroments/development.rb +44 -0
  60. data/spec/test_app/config/initializers/lolita_translation.rb +4 -0
  61. data/spec/test_app/config/initializers/token.rb +7 -0
  62. data/spec/test_app/config/routes.rb +4 -0
  63. data/spec/test_app/db/.gitkeep +0 -0
  64. data/spec/test_app/log/.gitkeep +0 -0
  65. metadata +193 -46
  66. data/.document +0 -5
  67. data/VERSION +0 -1
  68. data/app/views/components/lolita/translation/_assets.html.erb +0 -7
  69. data/app/views/components/lolita/translation/_language_wrap.html.erb +0 -4
  70. data/app/views/components/lolita/translation/_switch.html.erb +0 -8
  71. data/lib/generators/lolita_translation/USAGE +0 -8
  72. data/lib/generators/lolita_translation/has_translations_generator.rb +0 -8
  73. data/lib/lolita-translation/model.rb +0 -100
  74. data/lib/lolita-translation/modules.rb +0 -130
  75. data/lib/lolita-translation/string.rb +0 -19
  76. data/lib/tasks/has_translations_tasks.rake +0 -4
  77. data/spec/has_translations_spec.rb +0 -43
File without changes
@@ -1,192 +1,67 @@
1
+ require 'lolita-translation/translation_class_builder'
1
2
  module Lolita
2
3
  module Translation
3
4
 
4
- module SingletonMethods
5
- # Provides ability to add the translations for the model using delegate pattern.
6
- # Uses has_many association to the ModelNameTranslation.
7
- #
8
- # For example you have model Article with attributes title and text.
9
- # You want that attributes title and text to be translated.
10
- # For this reason you need to generate new model ArticleTranslation.
11
- # In migration you need to add:
12
- #
13
- # create_table :article_translations do |t|
14
- # t.references :article, :null => false
15
- # t.string :locale, :length => 2, :null => false
16
- # t.string :name, :null => false
17
- # end
18
- #
19
- # add_index :articles, [:article_id, :locale], :unique => true, :name => 'unique_locale_for_article_id'
20
- #
21
- # And in the Article model:
22
- #
23
- # translations :title, :text
24
- #
25
- # This will adds:
26
- #
27
- # * named_scope (translated) and has_many association to the Article model
28
- # * locale presence validation to the ArticleTranslation model.
29
- #
30
- # Notice: if you want to have validates_presence_of :article, you should use :inverse_of.
31
- # Support this by yourself. Better is always to use artile.translations.build() method.
32
- #
33
- # For more information please read API. Feel free to write me an email to:
34
- # dmitry.polushkin@gmail.com.
35
- #
36
- # ===
37
- #
38
- # You also can pass attributes and options to the translations class method:
39
- #
40
- # translations :title, :text, :fallback => true, :writer => true, :nil => nil
41
- #
42
- # ===
43
- #
44
- # Configuration options:
45
- #
46
- # * <tt>:fallback</tt> - if translation for the current locale not found.
47
- # By default true.
48
- # Uses algorithm of fallback:
49
- # 0) current translation (using I18n.locale);
50
- # 1) default locale (using I18n.default_locale);
51
- # 2) :nil value (see <tt>:nil</tt> configuration option)
52
- # * <tt>:reader</tt> - add reader attributes to the model and delegate them
53
- # to the translation model columns. Add's fallback if it is set to true.
54
- # * <tt>:writer</tt> - add writer attributes to the model and assign them
55
- # to the translation model attributes.
56
- # * <tt>:nil</tt> - when reader cant find string, it returns by default an
57
- # empty string. If you want to change this setting for example to nil,
58
- # add :nil => nil
59
- #
60
- # ===
61
- #
62
- # When you are using <tt>:writer</tt> option, you can create translations using
63
- # update_attributes method. For example:
64
- #
65
- # Article.create!
66
- # Article.update_attributes(:title => 'title', :text => 'text')
67
- #
68
- # ===
69
- #
70
- # <tt>translated</tt> named_scope is useful when you want to find only those
71
- # records that are translated to a specific locale.
72
- # For example if you want to find all Articles that is translated to an english
73
- # language, you can write: Article.translated(:en)
74
- #
75
- # <tt>has_translation?(locale)</tt> method, that returns true if object's model
76
- # have a translation for a specified locale
77
- #
78
- # <tt>translation(locale)</tt> method finds translation with specified locale.
79
- #
80
- # <tt>all_translations</tt> method that returns all possible translations in
81
- # ordered hash (useful when creating forms with nested attributes).
82
- def translations *attrs
83
- @translations_config ||= Lolita::Translation::Configuration.new(self,*attrs)
84
- if block_given?
85
- yield @translations_config
86
- end
87
- @translations_config
5
+ def self.included(base)
6
+ base.extend(Lolita::Translation::ORM::ClassMethods)
7
+ base.class_eval do
8
+ include Lolita::Translation::ORM::InstanceMethods
88
9
  end
89
10
  end
90
11
 
91
12
  class Configuration
92
-
93
- attr_reader :klass, :options, :translation_class_name, :translation_class,:attrs
94
-
95
- def initialize(base_class, *attrs)
96
- @klass = base_class
97
- @attrs = attrs
98
- initialize_options
99
- include_modules
100
- initialize_default_attributes
101
- extend_klass
102
- extend_translation_class
103
- end
104
-
105
- private
106
-
107
- def extend_klass
108
- config = self
109
- @klass.class_eval do
110
-
111
- class_attribute :has_translations_options
112
- self.has_translations_options = config.options
113
-
114
- class_attribute :columns_has_translations
115
- self.columns_has_translations = (columns rescue []).collect{|col| col if config.attrs.include?(col.name.to_sym)}.compact
116
-
117
- class_attribute :translation_attrs
118
- self.translation_attrs = config.attrs
119
-
120
- has_many :translations, :class_name => config.translation_class_name, :foreign_key => config.translation_class.master_id, :dependent => :destroy
121
- accepts_nested_attributes_for :translations, :allow_destroy => true, :reject_if => proc { |attributes| columns_has_translations.collect{|col| attributes[col.name].blank? ? nil : 1}.compact.empty? }
122
-
123
- scope :translated, lambda { |locale|
124
- where("#{translation_class.table_name}.locale = ?", locale.to_s).joins(:translations)
125
- }
126
-
127
- class << self
128
- alias_method_chain :find, :translations
13
+ attr_reader :klass, :attributes, :translation_class, :options
14
+ alias :translation_attributes :attributes
15
+
16
+ def initialize(base_klass, *args, &block)
17
+ @options = (args.respond_to?(:last) && args.last.is_a?(Hash) && args.pop) || {}
18
+ @klass = base_klass
19
+ @attributes = args
20
+ if base_klass.table_exists?
21
+ build_translation_class
22
+ if block_given?
23
+ block.call(self)
129
24
  end
130
25
  end
131
- override_readers
132
26
  end
133
27
 
134
- def extend_translation_class
135
- translation_class.belongs_to @klass.name.demodulize.underscore.to_sym
136
- translation_class.validates_presence_of :locale
137
- translation_class.validates_uniqueness_of :locale, :scope => translation_class.master_id
28
+ def association_name
29
+ options[:association_name] || :translations
138
30
  end
139
31
 
140
- def override_readers
141
- if options[:reader]
142
- attrs.each do |name|
143
- override_reader(name)
144
- end
32
+ def locales
33
+ if options[:locales]
34
+ Lolita::Translation::Locales.new(options[:locales])
35
+ else
36
+ Lolita::Translation.locales
145
37
  end
146
38
  end
147
39
 
148
- def override_reader(name)
149
- @klass.send :define_method, name do
150
- unless ::I18n.default_locale == ::I18n.locale
151
- translation = self.translation(::I18n.locale)
152
- if translation.nil?
153
- if has_translations_options[:fallback]
154
- (self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name) else
155
- has_translations_options[:nil]
156
- end
157
- else
158
- if @return_raw_data
159
- (self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
160
- else
161
- value = translation.send(name) and value.set_origins(self,name)
162
- end
163
- end
164
- else
165
- (self[name].nil? || self[name].blank?) ? has_translations_options[:nil] : self[name].set_origins(self,name)
166
- end
167
- end
40
+ def locale_field_name
41
+ options[:locale_field_name] || :default_locale
168
42
  end
169
43
 
170
- def initialize_default_attributes
171
- @translation_class_name = "#{@klass.name}Translation"
172
- @translation_class = Lolita::Translation::TranslationModel.new(self).klass
44
+ def association_key
45
+ options[:association_key] || :"#{demodulized_class_name}_id"
173
46
  end
174
47
 
175
- def include_modules
176
- @klass.send(:include, Lolita::Translation::InstanceMethods)
177
- @klass.extend(Lolita::Translation::ClassMethods)
48
+ def table_name
49
+ options[:table_name] || "#{klass.table_name}_translations"
178
50
  end
179
51
 
180
- def initialize_options
181
- @options = {
182
- :fallback => true,
183
- :reader => true,
184
- :writer => false,
185
- :nil => ''
186
- }.merge(@attrs.extract_options!)
187
- @options.assert_valid_keys([:fallback, :reader, :writer, :nil,:table_name])
52
+ def demodulized_class_name
53
+ klass.to_s.demodulize.underscore
54
+ end
55
+
56
+ private
57
+
58
+ def build_translation_class
59
+ @builder = Lolita::Translation::TranslationClassBuilder.new(self.klass, self)
60
+ @translation_class = @builder.build_class
61
+ @builder.override_attributes(@attributes)
188
62
  end
189
63
 
190
64
  end
65
+
191
66
  end
192
- end
67
+ end
@@ -0,0 +1,15 @@
1
+ module Lolita
2
+ module Translation
3
+ class ConfigurationNotInitializedError < StandardError
4
+ def initialize(klass)
5
+ super %Q{Configuration is not initialized for #{klass}. Try to call #translate in class.}
6
+ end
7
+ end
8
+
9
+ class NoBuilderForClassError < ArgumentError
10
+ def initialize(klass)
11
+ super %Q{No builder for #{klass}. See available builder in /lib/lolita-translation/builder directory.}
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,32 @@
1
+ require 'i18n'
2
+
3
+ module Lolita
4
+ module Translation
5
+
6
+ class Locale
7
+ attr_accessor :name
8
+ alias :short_name :name
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ end
13
+
14
+ def humanized_short_name
15
+ self.name.to_s.sub(/^(\w{1})(\w+)/) do
16
+ "#{$1.to_s.upcase}#{$2}"
17
+ end
18
+ end
19
+
20
+ def active?
21
+ self.name == current_locale
22
+ end
23
+
24
+ private
25
+
26
+ def current_locale
27
+ ::I18n.locale
28
+ end
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,62 @@
1
+ require 'lolita-translation/locale'
2
+
3
+ module Lolita
4
+ module Translation
5
+
6
+ class Locales
7
+ include Enumerable
8
+
9
+ def initialize(locale_names)
10
+ @locale_names = locale_names
11
+ end
12
+
13
+ def each
14
+ populate_locales!
15
+ @locales.each do |locale|
16
+ yield locale
17
+ end
18
+ end
19
+
20
+ def by_resource_locale(resource)
21
+ r_locale = resource_locale(resource)
22
+ self.inject([]) do |result, locale|
23
+ if r_locale.to_s == locale.name.to_s
24
+ result.unshift(locale)
25
+ else
26
+ result.push(locale)
27
+ end
28
+ result
29
+ end
30
+ end
31
+
32
+ def locale_names
33
+ l_names = if @locale_names.respond_to?(:call)
34
+ @locale_names.call
35
+ else
36
+ @locale_names
37
+ end
38
+ l_names.sort
39
+ end
40
+
41
+ def active
42
+ self.detect{|locale| locale.active?}
43
+ end
44
+
45
+ private
46
+
47
+ def resource_locale(resource)
48
+ transl_record = resource.translation_record
49
+ transl_record && transl_record.default_locale || ::I18n.locale
50
+ end
51
+
52
+ def populate_locales!
53
+ unless @locales
54
+ @locales = locale_names.map do |locale_name|
55
+ Lolita::Translation::Locale.new(locale_name)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,49 @@
1
+ Lolita::Hooks.component(:"/lolita/configuration/tab/error_msg").before do
2
+ tab = self.component_locals[:tab]
3
+ if tab.translatable?
4
+ self.send(:render_component,"lolita/translation",:switch, :tab => tab)
5
+ end
6
+ end
7
+
8
+ Lolita::Hooks.component(:"/lolita/configuration/tab/fields").after do
9
+ tab = self.component_locals[:tab]
10
+ if tab.translatable?
11
+ self.render_component tab.build_translations_nested_form(self.resource)
12
+ end
13
+ end
14
+
15
+ Lolita::Hooks.component(:"/lolita/configuration/tab/fields").around do
16
+ tab = self.component_locals[:tab]
17
+ if tab.translatable?
18
+ content = nil
19
+ resource.in(resource.original_locale) do
20
+ content = let_content
21
+ end
22
+ self.send(:render_component,"lolita/translation",:language_wrap, {
23
+ :tab => tab,
24
+ :content => content,
25
+ :active => true,
26
+ :translation_locale => resource.original_locale
27
+ })
28
+ else
29
+ let_content
30
+ end
31
+ end
32
+
33
+ Lolita::Hooks.component(:"/lolita/configuration/nested_form/fields").around do
34
+ tab = self.component_locals[:nested_form].parent
35
+ if tab.translatable? && current_form.object.respond_to?(:locale)
36
+ self.send(:render_component,"lolita/translation",:language_wrap, {
37
+ :tab => tab,
38
+ :content => let_content,
39
+ :active => false,
40
+ :translation_locale => current_form.object.locale
41
+ })
42
+ else
43
+ let_content
44
+ end
45
+ end
46
+
47
+ Lolita::Hooks.component(:"/lolita/configuration/tabs/display").before do
48
+ self.render_component "lolita/translation", :assets
49
+ end
@@ -0,0 +1,113 @@
1
+ module Lolita
2
+ module Configuration
3
+ module Tab
4
+
5
+ class Base
6
+
7
+ # This class encapsulate all logic about tab that are translatable, original tab uses it only when it is neccessary
8
+ class TranslationTabExtension
9
+ attr_reader :tab, :dbi
10
+ def initialize(tab)
11
+ @tab = tab
12
+ @dbi = tab.dbi
13
+ end
14
+
15
+ def translatable?
16
+ is_dbi_klass_translatable? && tab_has_translatable_fields?
17
+ end
18
+
19
+ def build_form(resource)
20
+ resource.build_nested_translations
21
+ nested_form = create_translations_nested_form(resource)
22
+ nested_form
23
+ end
24
+
25
+ def add_original_locale_field(fields)
26
+ if is_dbi_klass_translatable? && !exist_original_locale_field?(fields) && tab_has_translatable_fields?(fields)
27
+ locale_field = Lolita::Configuration::Factory::Field.add(dbi, :original_locale, :string, :builder => :hidden)
28
+ fields << locale_field
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def create_translations_nested_form(resource)
35
+ nested_form = Lolita::Configuration::NestedForm.new(tab, translations_association_name)
36
+ nested_form.expandable = false
37
+ nested_form.field_style = :normal
38
+ nested_form.fields = fields_for_translation_nested_form(nested_form)
39
+ nested_form
40
+ end
41
+
42
+ def fields_for_translation_nested_form(nested_form)
43
+ t_attributes = dbi_klass_translation_attributes
44
+ t_fields = tab.fields.reject do|field|
45
+ !t_attributes.include?(field.name.to_sym)
46
+ end
47
+ t_fields << Lolita::Configuration::Factory::Field.add(nested_form.dbi,:locale,:string, :builder => :hidden)
48
+ t_fields
49
+ end
50
+
51
+ def exist_original_locale_field?(fields)
52
+ !!fields.detect do |field|
53
+ field.name == :original_locale
54
+ end
55
+ end
56
+
57
+ def is_dbi_klass_translatable?
58
+ (dbi.klass.respond_to?(:translations_configuration) && dbi.klass.respond_to?(:translate))
59
+ end
60
+
61
+ def tab_has_translatable_fields?(fields = nil)
62
+ (collect_possibly_translateble_fields(fields) & dbi_klass_translation_attributes).any?
63
+ end
64
+
65
+ def dbi_klass_translation_attributes
66
+ transl_configuration = dbi.klass.translations_configuration
67
+ transl_configuration && transl_configuration.attributes || []
68
+ end
69
+
70
+ def translations_association_name
71
+ translations_configuration.association_name
72
+ end
73
+
74
+ def locale_field_name
75
+ translations_configuration.locale_field_name
76
+ end
77
+
78
+ def translations_configuration
79
+ dbi.klass.translations_configuration
80
+ end
81
+
82
+
83
+ def collect_possibly_translateble_fields(fields = nil)
84
+ (fields || tab.fields).reject{|field|
85
+ field.dbi != dbi
86
+ }.map(&:name)
87
+ end
88
+
89
+ end
90
+
91
+ def fields
92
+ translation_tab_extension.add_original_locale_field(@fields)
93
+ @fields
94
+ end
95
+
96
+ def translatable?
97
+ translation_tab_extension.translatable?
98
+ end
99
+
100
+ def build_translations_nested_form(resource)
101
+ translation_tab_extension.build_form(resource)
102
+ end
103
+
104
+ private
105
+
106
+ def translation_tab_extension
107
+ @translation_tab_extension ||= TranslationTabExtension.new(self)
108
+ end
109
+ end
110
+
111
+ end
112
+ end
113
+ end