dm-is-localizable 0.10.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.
data/README.textile ADDED
@@ -0,0 +1,260 @@
1
+ h1. dm-is-localizable
2
+
3
+ Datamapper support for localization of (user entered) content in multilanguage applications
4
+
5
+ h3. Schema
6
+
7
+ * one xxx_translations table for every translatable resource
8
+ * xxx_translations belongs_to the resource to translate
9
+ * xxx_translations belongs_to a language
10
+ * properties to be translated are defined in xxx_translations
11
+
12
+
13
+ h4. Advantages
14
+
15
+ * Proper normalization and referential integrity
16
+ * Ease in adding a new language (add row to xxx_translations)
17
+ * Easy to query
18
+ * Columns keep their names
19
+
20
+
21
+ h4. Disadvantages (not really if you think about it)
22
+
23
+ * One table for every resource that needs translations
24
+
25
+
26
+ h3. Example definition of a localizable model
27
+
28
+ Currently, you need to define a @Language@ model yourself, to get @dm-is-localizable@ started. However, this is reasonably easy! If you do a @rake install@ after you cloned the repo (I guess it won't work if you do a simple @gem install@), it will print out the code for language.rb and will tell you where to put it.
29
+
30
+ <pre>
31
+ <code>
32
+ class Language
33
+
34
+ include DataMapper::Resource
35
+
36
+ # properties
37
+
38
+ property :id, Serial
39
+
40
+ property :code, String, :required => true, :unique => true, :unique_index => true
41
+ property :name, String, :required => true
42
+
43
+ # locale string like 'en-US'
44
+ validates_format :code, :with => /^[a-z]{2}-[A-Z]{2}$/
45
+
46
+
47
+ def self.[](code)
48
+ return nil if code.nil?
49
+ first :code => code.to_s.gsub('_', '-')
50
+ end
51
+
52
+ end
53
+ </code>
54
+ </pre>
55
+
56
+ Once you have this model in place, you can start defining your _localizable models_.
57
+
58
+ <pre>
59
+ <code>
60
+ class Item
61
+
62
+ include DataMapper::Resource
63
+
64
+ property :id, Serial
65
+
66
+ is :localizable do # same as is :localizable, :accept_nested_attributes => true do
67
+ property :name, String
68
+ property :desc, String
69
+ end
70
+
71
+ end
72
+ </code>
73
+ </pre>
74
+
75
+ The above @Item@ model will define and thus be able to @DataMapper.auto_migrate!@ the @ItemTranslation@ model. The _naming convention_ used here is @"#{ClassToBeLocalized.name}Translation"@.
76
+
77
+ Preliminary support for changing this is available by using the @:model@ option like so (note that this isn't specced yet).
78
+
79
+ <pre>
80
+ <code>
81
+ DataMapper::Model.is :localizable, :model => 'ItemLocalization'
82
+ </code>
83
+ </pre>
84
+
85
+ Furthermore, the above @Item@ will automatically have the following instance methods defined.
86
+
87
+ <pre>
88
+ <code>
89
+ #item_translations_attributes
90
+ #item_translations_attributes=
91
+
92
+ # and handy aliases for the above
93
+
94
+ #translations_attributes
95
+ #translations_attributes=
96
+ </code>
97
+ </pre>
98
+
99
+ These are generated by "dm-accepts_nested_attributes":http://github.com/snusnu/dm-accepts_nested_attributes and allow for easy manipulation of the localizable properties from say forms in a web application. For more information on working with nested attributes, have a look at the documentation at the "README":http://github.com/snusnu/dm-accepts_nested_attributes for "dm-accepts_nested_attributes":http://github.com/snusnu/dm-accepts_nested_attributes
100
+
101
+ Of course you can turn this behavior off by specifying the @is :localizable, :accept_nested_attributes => false do .. end@
102
+
103
+ The resulting model you get when calling @Item.is(:localizable)@ looks like this:
104
+
105
+ <pre>
106
+ <code>
107
+ class ItemTranslation
108
+
109
+ include DataMapper::Resource
110
+
111
+ property :id, Serial
112
+
113
+ property :item_id, Integer, :required => true, :unique_index => :unique_languages
114
+ property :language_id, Integer, :required => true, :unique_index => :unique_languages
115
+
116
+ property :name, String
117
+ property :desc, String
118
+
119
+ validates_is_unique :language_id, :scope => :item_id
120
+
121
+ belongs_to :item
122
+ belongs_to :language
123
+
124
+ end
125
+ </code>
126
+ </pre>
127
+
128
+ Furthermore, the following API gets defined on the @Item@ class:
129
+
130
+ <pre>
131
+ <code>
132
+ class Item
133
+
134
+ include DataMapper::Resource
135
+
136
+ property :id, Serial
137
+
138
+ is :localizable do
139
+ property :name, String
140
+ property :desc, String
141
+ end
142
+
143
+ # ----------------------------
144
+ # added by is :localizable
145
+ # ----------------------------
146
+
147
+ has n, :item_translations
148
+ has n, :languages, :through => :item_translations
149
+
150
+ # and a handy alias
151
+ alias :translations :item_translations
152
+
153
+ # helper to get at ItemTranslation
154
+ class_inheritable_reader :translation_model
155
+
156
+ # -------------------
157
+ # class level API
158
+ # -------------------
159
+
160
+ # list all available languages for Items
161
+ def self.available_languages
162
+ Language.all :id => translation_model.all.map { |t| t.language_id }.uniq
163
+ end
164
+
165
+ # the number of all available languages for the localizable model
166
+ def self.nr_of_available_languages
167
+ available_languages.size
168
+ end
169
+
170
+ # checks if all localizable resources are translated in all available languages
171
+ def self.translations_complete?
172
+ nr_of_available_languages * all.size == translation_model.all.size
173
+ end
174
+
175
+ # returns a list of symbols reflecting all localizable property names of this resource
176
+ def localizable_properties
177
+ translation_model.properties.map do |p|
178
+ p.name
179
+ end.select do |p|
180
+ # exclude properties that are'nt localizable
181
+ p != :id && p != :language_id && p != Extlib::Inflection.foreign_key(self.name).to_sym
182
+ end
183
+ end
184
+
185
+ # ----------------------
186
+ # instance level API
187
+ # ----------------------
188
+
189
+ # list all available languages for this instance
190
+ def available_languages
191
+ Language.all :id => translations.map { |t| t.language_id }.uniq
192
+ end
193
+
194
+ # the number of all available languages for this instance
195
+ def nr_of_available_languages
196
+ available_languages.size
197
+ end
198
+
199
+ # checks if this instance is translated into all available languages for this model
200
+ def translations_complete?
201
+ self.class.nr_of_available_languages == translations.size
202
+ end
203
+
204
+ # translates the given attribute to the language identified by the given language_code
205
+ def translate(attribute, language_code)
206
+ if language = Language[language_code]
207
+ t = translations.first(:language_id => language.id)
208
+ t.respond_to?(attribute) ? t.send(attribute) : nil
209
+ else
210
+ nil
211
+ end
212
+ end
213
+
214
+ # translates the :name property to the given language
215
+ def name(language_code)
216
+ translate(:name, language_code)
217
+ end
218
+
219
+ # translates the :desc property to the given language
220
+ def desc(language_code)
221
+ translate(:desc, language_code)
222
+ end
223
+
224
+
225
+ # ----------------------------------------
226
+ # added by dm-accepts_nested_attributes
227
+ # ----------------------------------------
228
+
229
+
230
+ def item_translations_attributes
231
+ # ...
232
+ end
233
+
234
+ def item_translations_attributes=(attributes_or_attributes_collection)
235
+ # ...
236
+ end
237
+
238
+ # and handy aliases for the above
239
+
240
+ alias :translations_attributes :item_translations_attributes
241
+ alias :translations_attributes= :item_translations_attributes
242
+
243
+
244
+ # TODO
245
+ # more API to support common usecases (and i18n/l10n solutions)
246
+
247
+ end
248
+ </code>
249
+ </pre>
250
+
251
+ h3. Inspired by (thx guys!)
252
+
253
+ * Neil Barnwell's comment on the top voted answer to "Schema for a multilanguage database":http://stackoverflow.com/questions/316780/schema-for-a-multilanguage-database
254
+
255
+ * Gabi Solomon's option (4) at this blog post on "Multilanguage database design approach":http://www.gsdesign.ro/blog/multilanguage-database-design-approach/
256
+
257
+
258
+ h3. Copyright
259
+
260
+ Copyright (c) 2009 Martin Gamsjaeger (snusnu). See "LICENSE":http://github.com/snusnu/dm-is-localizable/tree/master/LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,86 @@
1
+ require 'pathname'
2
+ require 'rake'
3
+
4
+ ROOT = Pathname(__FILE__).dirname.expand_path
5
+ JRUBY = RUBY_PLATFORM =~ /java/
6
+
7
+ begin
8
+
9
+ require 'jeweler'
10
+
11
+ Jeweler::Tasks.new do |gem|
12
+
13
+ gem.name = "dm-is-localizable"
14
+ gem.summary = %Q{Datamapper support for localization of content in multilanguage applications}
15
+ gem.email = "gamsnjaga@gmail.com"
16
+ gem.homepage = "http://github.com/snusnu/dm-is-localizable"
17
+ gem.authors = ["Martin Gamsjaeger (snusnu)"]
18
+
19
+ gem.add_dependency('dm-core', '>= 0.10.2')
20
+ gem.add_dependency('dm-is-remixable', '>= 0.10.2')
21
+ gem.add_dependency('dm-validations', '>= 0.10.2')
22
+
23
+ gem.add_development_dependency 'rspec', '~> 1.3'
24
+
25
+ Jeweler::GemcutterTasks.new
26
+
27
+ end
28
+
29
+ rescue LoadError
30
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
31
+ end
32
+
33
+ begin
34
+ require 'spec/rake/spectask'
35
+
36
+ task :default => [ :spec ]
37
+
38
+ desc 'Run specifications'
39
+ Spec::Rake::SpecTask.new(:spec) do |t|
40
+ t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
41
+ t.libs << 'lib' << 'spec' # needed for CI rake spec task, duplicated in spec_helper
42
+
43
+ begin
44
+ require 'rcov'
45
+ t.rcov = JRUBY ? false : (ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : true)
46
+ t.rcov_opts << '--exclude' << 'spec'
47
+ t.rcov_opts << '--text-summary'
48
+ t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse'
49
+ rescue LoadError
50
+ # rcov not installed
51
+ rescue SyntaxError
52
+ # rcov syntax invalid
53
+ end
54
+ end
55
+ rescue LoadError
56
+ # rspec not installed
57
+ end
58
+
59
+ begin
60
+ require 'cucumber/rake/task'
61
+ Cucumber::Rake::Task.new(:features)
62
+ rescue LoadError
63
+ task :features do
64
+ abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
65
+ end
66
+ end
67
+
68
+ task :default => :spec
69
+
70
+ require 'rake/rdoctask'
71
+ Rake::RDocTask.new do |rdoc|
72
+ if File.exist?('VERSION.yml')
73
+ config = YAML.load(File.read('VERSION.yml'))
74
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
75
+ else
76
+ version = ""
77
+ end
78
+
79
+ rdoc.rdoc_dir = 'rdoc'
80
+ rdoc.title = "dm-is-localizable #{version}"
81
+ rdoc.rdoc_files.include('README*')
82
+ rdoc.rdoc_files.include('lib/**/*.rb')
83
+ end
84
+
85
+ # require all tasks below tasks
86
+ Pathname.glob(ROOT.join('tasks/**/*.rb').to_s).each { |f| require f }
data/TODO ADDED
@@ -0,0 +1,2 @@
1
+ * Add accessors for the localized properties in the localizable resource
2
+ * Think about validation and null constraints
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.10.1
@@ -0,0 +1,88 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{dm-is-localizable}
8
+ s.version = "0.10.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Martin Gamsjaeger (snusnu)"]
12
+ s.date = %q{2010-02-04}
13
+ s.email = %q{gamsnjaga@gmail.com}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.textile",
17
+ "TODO"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "CHANGELOG",
23
+ "LICENSE",
24
+ "README.textile",
25
+ "Rakefile",
26
+ "TODO",
27
+ "VERSION",
28
+ "dm-is-localizable.gemspec",
29
+ "lib/dm-is-localizable.rb",
30
+ "lib/dm-is-localizable/is/localizable.rb",
31
+ "lib/dm-is-localizable/storage/language.rb",
32
+ "lib/dm-is-localizable/storage/translation.rb",
33
+ "spec/fixtures/item.rb",
34
+ "spec/lib/rspec_tmbundle_support.rb",
35
+ "spec/shared/shared_examples_spec.rb",
36
+ "spec/spec.opts",
37
+ "spec/spec_helper.rb",
38
+ "spec/unit/auto_migrate_spec.rb",
39
+ "spec/unit/class_level_api_spec.rb",
40
+ "spec/unit/instance_level_api_spec.rb",
41
+ "spec/unit/is_localizable_spec.rb",
42
+ "spec/unit/language_spec.rb",
43
+ "spec/unit/translation_spec.rb",
44
+ "tasks/changelog.rb",
45
+ "tasks/install.rb",
46
+ "tasks/whitespace.rb"
47
+ ]
48
+ s.homepage = %q{http://github.com/snusnu/dm-is-localizable}
49
+ s.rdoc_options = ["--charset=UTF-8"]
50
+ s.require_paths = ["lib"]
51
+ s.rubygems_version = %q{1.3.5}
52
+ s.summary = %q{Datamapper support for localization of content in multilanguage applications}
53
+ s.test_files = [
54
+ "spec/fixtures/item.rb",
55
+ "spec/lib/rspec_tmbundle_support.rb",
56
+ "spec/shared/shared_examples_spec.rb",
57
+ "spec/spec_helper.rb",
58
+ "spec/unit/auto_migrate_spec.rb",
59
+ "spec/unit/class_level_api_spec.rb",
60
+ "spec/unit/instance_level_api_spec.rb",
61
+ "spec/unit/is_localizable_spec.rb",
62
+ "spec/unit/language_spec.rb",
63
+ "spec/unit/translation_spec.rb"
64
+ ]
65
+
66
+ if s.respond_to? :specification_version then
67
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
68
+ s.specification_version = 3
69
+
70
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
71
+ s.add_runtime_dependency(%q<dm-core>, [">= 0.10.2"])
72
+ s.add_runtime_dependency(%q<dm-is-remixable>, [">= 0.10.2"])
73
+ s.add_runtime_dependency(%q<dm-validations>, [">= 0.10.2"])
74
+ s.add_development_dependency(%q<rspec>, ["~> 1.3"])
75
+ else
76
+ s.add_dependency(%q<dm-core>, [">= 0.10.2"])
77
+ s.add_dependency(%q<dm-is-remixable>, [">= 0.10.2"])
78
+ s.add_dependency(%q<dm-validations>, [">= 0.10.2"])
79
+ s.add_dependency(%q<rspec>, ["~> 1.3"])
80
+ end
81
+ else
82
+ s.add_dependency(%q<dm-core>, [">= 0.10.2"])
83
+ s.add_dependency(%q<dm-is-remixable>, [">= 0.10.2"])
84
+ s.add_dependency(%q<dm-validations>, [">= 0.10.2"])
85
+ s.add_dependency(%q<rspec>, ["~> 1.3"])
86
+ end
87
+ end
88
+
@@ -0,0 +1,135 @@
1
+ module DataMapper
2
+ module Is
3
+
4
+ module Localizable
5
+
6
+
7
+ def is_localizable(options = {}, &block)
8
+
9
+ extend ClassMethods
10
+ include InstanceMethods
11
+
12
+ options = {
13
+ :as => nil,
14
+ :model => "#{self}Translation",
15
+ :accept_nested_attributes => true
16
+ }.merge(options)
17
+
18
+ remixer_fk = Extlib::Inflection.foreign_key(self.name).to_sym
19
+ remixer = remixer_fk.to_s.gsub('_id', '').to_sym
20
+ remixee = Extlib::Inflection.tableize(options[:model]).to_sym
21
+
22
+ remix n, Translation, :as => options[:as], :model => options[:model]
23
+
24
+ @translation_model = Extlib::Inflection.constantize(options[:model])
25
+
26
+ enhance :translation, @translation_model do
27
+
28
+ property remixer_fk, Integer, :min => 1, :required => true, :unique_index => :unique_languages
29
+ property :language_id, Integer, :min => 1, :required => true, :unique_index => :unique_languages
30
+
31
+ belongs_to remixer
32
+ belongs_to :language
33
+
34
+ class_eval &block
35
+
36
+ validates_is_unique :language_id, :scope => remixer_fk
37
+
38
+ end
39
+
40
+ has n, :languages, :through => remixee, :constraint => :destroy
41
+
42
+ self.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
43
+
44
+ alias :translations :#{remixee}
45
+
46
+ if options[:accept_nested_attributes]
47
+
48
+ # cannot accept_nested_attributes_for :translations
49
+ # since this is no valid relationship name, only an alias
50
+
51
+ accepts_nested_attributes_for :#{remixee}
52
+ alias :translations_attributes :#{remixee}_attributes
53
+
54
+ end
55
+
56
+ RUBY
57
+
58
+ localizable_properties.each do |property_name|
59
+ self.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
60
+
61
+ def #{property_name}(language_code)
62
+ translate(:#{property_name.to_sym}, language_code)
63
+ end
64
+
65
+ RUBY
66
+ end
67
+
68
+ end
69
+
70
+ module ClassMethods
71
+
72
+ def translation_model
73
+ @translation_model
74
+ end
75
+
76
+ # list all available languages for the localizable model
77
+ def available_languages
78
+ ids = translation_model.all.map { |t| t.language_id }.uniq
79
+ ids.empty? ? [] : Language.all(:id => ids)
80
+ end
81
+
82
+ # the number of all available languages for the localizable model
83
+ def nr_of_available_languages
84
+ available_languages.size
85
+ end
86
+
87
+ # checks if all localizable resources are translated in all available languages
88
+ def translations_complete?
89
+ available_languages.size * all.size == translation_model.all.size
90
+ end
91
+
92
+ # returns a list of symbols reflecting all localizable property names of this resource
93
+ def localizable_properties
94
+ translation_model.properties.map { |p| p.name }.select do |p|
95
+ # exclude properties that are'nt localizable
96
+ p != :id && p != :language_id && p != Extlib::Inflection.foreign_key(self.name).to_sym
97
+ end
98
+ end
99
+
100
+ end
101
+
102
+ module InstanceMethods
103
+
104
+ # list all available languages for this instance
105
+ def available_languages
106
+ ids = translations.map { |t| t.language_id }.uniq
107
+ ids.empty? ? [] : Language.all(:id => ids)
108
+ end
109
+
110
+ # the number of all available languages for this instance
111
+ def nr_of_available_languages
112
+ available_languages.size
113
+ end
114
+
115
+ # checks if this instance is translated into all available languages for this model
116
+ def translations_complete?
117
+ self.class.nr_of_available_languages == translations.size
118
+ end
119
+
120
+ # translates the given attribute to the language identified by the given language_code
121
+ def translate(attribute, language_code)
122
+ if language = Language[language_code]
123
+ t = translations.first(:language_id => language.id)
124
+ t.respond_to?(attribute) ? t.send(attribute) : nil
125
+ else
126
+ nil
127
+ end
128
+ end
129
+
130
+ end
131
+
132
+ end
133
+
134
+ end
135
+ end
@@ -0,0 +1,21 @@
1
+ class Language
2
+
3
+ include DataMapper::Resource
4
+
5
+ # properties
6
+
7
+ property :id, Serial
8
+
9
+ property :code, String, :required => true, :unique => true, :unique_index => true
10
+ property :name, String, :required => true
11
+
12
+ # locale string like 'en-US'
13
+ validates_format :code, :with => /^[a-z]{2}-[A-Z]{2}$/
14
+
15
+
16
+ def self.[](code)
17
+ return nil if code.nil?
18
+ first :code => code.to_s.gsub('_', '-')
19
+ end
20
+
21
+ end
@@ -0,0 +1,18 @@
1
+ module DataMapper
2
+ module Is
3
+ module Localizable
4
+
5
+ module Translation
6
+
7
+ include DataMapper::Resource
8
+
9
+ is :remixable
10
+
11
+ property :id, Serial
12
+ property :language_id, Integer, :min => 1, :required => false
13
+
14
+ end
15
+
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ require 'dm-is-localizable/is/localizable'
2
+ require 'dm-is-localizable/storage/language'
3
+ require 'dm-is-localizable/storage/translation'
4
+
5
+ # Include the plugin in Model
6
+ DataMapper::Model.append_extensions DataMapper::Is::Localizable
@@ -0,0 +1,12 @@
1
+ class Item
2
+
3
+ include DataMapper::Resource
4
+
5
+ property :id, Serial
6
+
7
+ is :localizable do
8
+ property :name, String
9
+ property :desc, String
10
+ end
11
+
12
+ end
@@ -0,0 +1,35 @@
1
+ # -----------------------------------------------
2
+ # support for nice html output in rspec tmbundle
3
+ # -----------------------------------------------
4
+
5
+ module RSpecTmBundleHelpers
6
+
7
+ class TextmateRspecLogger < DataMapper::Logger
8
+ def prep_msg(message, level)
9
+ "#{super}<br />"
10
+ end
11
+ end
12
+
13
+ def with_dm_logger(level = :debug)
14
+ DataMapper.logger.level = level
15
+ yield
16
+ ensure
17
+ DataMapper.logger.level = :off
18
+ end
19
+
20
+ def print_call_stack(from = 2, to = nil, html = true)
21
+ (from..(to ? to : caller.length)).each do |idx|
22
+ p "[#{idx}]: #{caller[idx]}#{html ? '<br />' : ''}"
23
+ end
24
+ end
25
+
26
+ def puth(html = nil)
27
+ print "#{h(html)}<br />"
28
+ end
29
+
30
+ ESCAPE_TABLE = { '&' => '&amp;', '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => '&#039;' }
31
+ def h(value)
32
+ value.to_s.gsub(/[&<>"]/) {|s| ESCAPE_TABLE[s] }
33
+ end
34
+
35
+ end