rails-i18nterface 0.1.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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +22 -0
  3. data/README.md +109 -0
  4. data/Rakefile +23 -0
  5. data/app/assets/javascripts/rails_i18nterface/application.js +8 -0
  6. data/app/assets/javascripts/rails_i18nterface/base.js +62 -0
  7. data/app/assets/javascripts/rails_i18nterface/ender.js +3260 -0
  8. data/app/assets/javascripts/rails_i18nterface/ender.min.js +45 -0
  9. data/app/assets/stylesheets/rails_i18nterface/application.css +450 -0
  10. data/app/controllers/rails_i18nterface/application_controller.rb +4 -0
  11. data/app/controllers/rails_i18nterface/translate_controller.rb +255 -0
  12. data/app/helpers/rails_i18nterface/application_helper.rb +4 -0
  13. data/app/helpers/rails_i18nterface/translate_helper.rb +74 -0
  14. data/app/models/translation.rb +4 -0
  15. data/app/views/layouts/rails_i18nterface/translate.html.erb +13 -0
  16. data/app/views/rails_i18nterface/translate/_namespaces.html.erb +1 -0
  17. data/app/views/rails_i18nterface/translate/_pagination.html.erb +18 -0
  18. data/app/views/rails_i18nterface/translate/index.html.erb +132 -0
  19. data/config/routes.rb +7 -0
  20. data/db/migrate/20110921112044_create_translations.rb +18 -0
  21. data/lib/rails-i18nterface.rb +8 -0
  22. data/lib/rails-i18nterface/engine.rb +7 -0
  23. data/lib/rails-i18nterface/file.rb +35 -0
  24. data/lib/rails-i18nterface/keys.rb +192 -0
  25. data/lib/rails-i18nterface/log.rb +35 -0
  26. data/lib/rails-i18nterface/storage.rb +29 -0
  27. data/lib/rails-i18nterface/version.rb +3 -0
  28. data/lib/tasks/rails-i18nterface.rake +4 -0
  29. data/spec/controllers/translate_controller_spec.rb +135 -0
  30. data/spec/internal/app/assets/javascripts/application.js +2 -0
  31. data/spec/internal/app/assets/stylesheets/application.css +3 -0
  32. data/spec/internal/app/controllers/application_controller.rb +4 -0
  33. data/spec/internal/app/models/article.rb +11 -0
  34. data/spec/internal/app/views/application/index.html.erb +5 -0
  35. data/spec/internal/app/views/categories/category.erb +1 -0
  36. data/spec/internal/app/views/categories/category.html +1 -0
  37. data/spec/internal/app/views/categories/category.html.erb +1 -0
  38. data/spec/internal/app/views/categories/category.rhtml +5 -0
  39. data/spec/internal/config/database.yml +3 -0
  40. data/spec/internal/config/routes.rb +4 -0
  41. data/spec/internal/db/combustion_test.sqlite +0 -0
  42. data/spec/internal/db/schema.rb +3 -0
  43. data/spec/internal/log/test.log +521 -0
  44. data/spec/internal/public/favicon.ico +0 -0
  45. data/spec/lib/file_spec.rb +53 -0
  46. data/spec/lib/keys_spec.rb +179 -0
  47. data/spec/lib/log_spec.rb +46 -0
  48. data/spec/lib/storage_spec.rb +33 -0
  49. data/spec/requests/search_spec.rb +62 -0
  50. data/spec/spec_helper.rb +37 -0
  51. metadata +200 -0
data/config/routes.rb ADDED
@@ -0,0 +1,7 @@
1
+ RailsI18nterface::Engine.routes.draw do
2
+ root :to => 'translate#index'
3
+
4
+ put '/translate' => 'translate#update'
5
+ get '/reload' => 'translate#reload', :as => 'translate_reload'
6
+ get '/export' => 'translate#export', :as => 'translate_export'
7
+ end
@@ -0,0 +1,18 @@
1
+ class CreateTranslations < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :translations do |t|
4
+ t.string :locale
5
+ t.string :key
6
+ t.text :value
7
+ t.text :interpolations
8
+ t.boolean :is_proc, :default => false
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+
14
+ def self.down
15
+ drop_table :translations
16
+ end
17
+ end
18
+
@@ -0,0 +1,8 @@
1
+ require "rails-i18nterface/engine"
2
+ require "rails-i18nterface/file"
3
+ require "rails-i18nterface/keys"
4
+ require "rails-i18nterface/log"
5
+ require "rails-i18nterface/storage"
6
+
7
+ module RailsI18nterface
8
+ end
@@ -0,0 +1,7 @@
1
+ require 'rails'
2
+
3
+ module RailsI18nterface
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace RailsI18nterface
6
+ end
7
+ end
@@ -0,0 +1,35 @@
1
+ require 'fileutils'
2
+
3
+ class RailsI18nterface::File
4
+ attr_accessor :path
5
+
6
+ def initialize(path)
7
+ self.path = path
8
+ end
9
+
10
+ def write(keys)
11
+ FileUtils.mkdir_p File.dirname(path)
12
+ File.open(path, "w") do |file|
13
+ file.puts keys_to_yaml(self.class.deep_stringify_keys(keys))
14
+ end
15
+ end
16
+
17
+ def read
18
+ File.exists?(path) ? YAML::load(IO.read(path)) : {}
19
+ end
20
+
21
+ # Stringifying keys for prettier YAML
22
+ def self.deep_stringify_keys(hash)
23
+ hash.inject({}) { |result, (key, value)|
24
+ value = deep_stringify_keys(value) if value.is_a? Hash
25
+ result[(key.to_s rescue key) || key] = value
26
+ result
27
+ }
28
+ end
29
+
30
+ private
31
+ def keys_to_yaml(keys)
32
+ # Using ya2yaml, if available, for UTF8 support
33
+ keys.respond_to?(:ya2yaml) ? keys.ya2yaml(:escape_as_utf8 => true) : keys.to_yaml
34
+ end
35
+ end
@@ -0,0 +1,192 @@
1
+ #require 'pathname'
2
+
3
+ class RailsI18nterface::Keys
4
+ # Allows keys extracted from lookups in files to be cached
5
+ def self.files
6
+ @@files ||= new.files
7
+ end
8
+
9
+ def self.names
10
+ @@names || new.names
11
+ end
12
+ # Allows flushing of the files cache
13
+ def self.files=(files)
14
+ @@files = files
15
+ end
16
+
17
+ def files
18
+ @files ||= extract_files
19
+ end
20
+ alias_method :to_hash, :files
21
+
22
+ def names
23
+ @names ||= build_namespaces
24
+ end
25
+ alias_method :to_tree, :names
26
+
27
+ def keys
28
+ files.keys
29
+ end
30
+ alias_method :to_a, :keys
31
+
32
+ def i18n_keys(locale)
33
+ I18n.backend.send(:init_translations) unless I18n.backend.initialized?
34
+ self.class.to_shallow_hash(I18n.backend.send(:translations)[locale.to_sym]).keys.sort
35
+ end
36
+
37
+ def untranslated_keys
38
+ self.class.translated_locales.inject({}) do |missing, locale|
39
+ missing[locale] = i18n_keys(I18n.default_locale).map do |key|
40
+ I18n.backend.send(:lookup, locale, key).nil? ? key : nil
41
+ end.compact
42
+ missing
43
+ end
44
+ end
45
+
46
+ def missing_keys
47
+ locale = I18n.default_locale
48
+ yaml_keys = {}
49
+ yaml_keys = RailsI18nterface::Storage.file_paths(locale).inject({}) do |keys, path|
50
+ keys = keys.deep_merge(RailsI18nterface::File.new(path).read[locale.to_s])
51
+ end
52
+ files.reject { |key, file| self.class.contains_key?(yaml_keys, key) }
53
+ end
54
+
55
+ def self.translated_locales
56
+ I18n.available_locales.reject { |locale| [:root, I18n.default_locale.to_sym].include?(locale) }
57
+ end
58
+
59
+ # Checks if a nested hash contains the keys in dot separated I18n key.
60
+ #
61
+ # Example:
62
+ #
63
+ # hash = {
64
+ # :foo => {
65
+ # :bar => {
66
+ # :baz => 1
67
+ # }
68
+ # }
69
+ # }
70
+ #
71
+ # contains_key?("foo", key) # => true
72
+ # contains_key?("foo.bar", key) # => true
73
+ # contains_key?("foo.bar.baz", key) # => true
74
+ # contains_key?("foo.bar.baz.bla", key) # => false
75
+ #
76
+ def self.contains_key?(hash, key)
77
+ keys = key.to_s.split(".")
78
+ return false if keys.empty?
79
+ !keys.inject(HashWithIndifferentAccess.new(hash)) do |memo, key|
80
+ memo.is_a?(Hash) ? memo.try(:[], key) : nil
81
+ end.nil?
82
+ end
83
+
84
+ # Convert something like:
85
+ #
86
+ # {
87
+ # :pressrelease => {
88
+ # :label => {
89
+ # :one => "Pressmeddelande"
90
+ # }
91
+ # }
92
+ # }
93
+ #
94
+ # to:
95
+ #
96
+ # {'pressrelease.label.one' => "Pressmeddelande"}
97
+ #
98
+ def self.to_shallow_hash(hash)
99
+ hash.inject({}) do |shallow_hash, (key, value)|
100
+ if value.is_a?(Hash)
101
+ to_shallow_hash(value).each do |sub_key, sub_value|
102
+ shallow_hash[[key, sub_key].join(".")] = sub_value
103
+ end
104
+ else
105
+ shallow_hash[key.to_s] = value
106
+ end
107
+ shallow_hash
108
+ end
109
+ end
110
+
111
+ # Convert something like:
112
+ #
113
+ # {'pressrelease.label.one' => "Pressmeddelande"}
114
+ #
115
+ # to:
116
+ #
117
+ # {
118
+ # :pressrelease => {
119
+ # :label => {
120
+ # :one => "Pressmeddelande"
121
+ # }
122
+ # }
123
+ # }
124
+ def self.to_deep_hash(hash)
125
+ hash.inject({}) do |deep_hash, (key, value)|
126
+ keys = key.to_s.split('.').reverse
127
+ leaf_key = keys.shift
128
+ key_hash = keys.inject({leaf_key.to_sym => value}) { |hash, key| {key.to_sym => hash} }
129
+ deep_merge!(deep_hash, key_hash)
130
+ deep_hash
131
+ end
132
+ end
133
+
134
+ # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
135
+ def self.deep_merge!(hash1, hash2)
136
+ merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
137
+ hash1.merge!(hash2, &merger)
138
+ end
139
+
140
+ private
141
+
142
+ def extract_files
143
+ files_to_scan.inject(HashWithIndifferentAccess.new) do |files, file|
144
+ begin#hack to avoid UTF-8 error
145
+ IO.read(file).scan(i18n_lookup_pattern).flatten.map(&:to_sym).each do |key|
146
+ path = Pathname.new(File.expand_path(file)).relative_path_from(Pathname.new(Rails.root)).to_s
147
+ #puts path
148
+ if key[0] == '.'
149
+ key = guess_namespace(path) + key.to_s
150
+ end
151
+ #puts key
152
+ #puts
153
+ files[key] ||= []
154
+ files[key] << path if !files[key].include?(path)
155
+ end
156
+ rescue Exception => e
157
+ puts e.inspect
158
+ puts e.backtrace
159
+ puts "bug in Translation plugin, please debug, informations :"
160
+ puts file.inspect
161
+ puts i18n_lookup_pattern.inspect
162
+ end
163
+ files
164
+ end
165
+ end
166
+
167
+ def guess_namespace(path)
168
+ parts = path.split(/\//)
169
+ case parts[0]
170
+ when 'app'
171
+ parts.shift
172
+ if parts[0] == 'views'
173
+ parts.shift
174
+ parts[-1].gsub!(/\.(erb|haml|slim)$/,'')
175
+ end
176
+ end
177
+ parts.join('.')
178
+ end
179
+
180
+ def build_namespaces
181
+ i18n_keys
182
+ end
183
+
184
+ def i18n_lookup_pattern
185
+ /\b(?:I18n\.t|I18n\.translate|t)(?:\s|\():?(?:'|")(\.?[a-z0-9_\.]+)(?:'|")/
186
+ end
187
+
188
+ def files_to_scan
189
+ Dir.glob(File.join(RailsI18nterface::Storage.root_dir, "{app,config,lib}", "**","*.{rb,erb,haml,slim,rhtml}")) +
190
+ Dir.glob(File.join(RailsI18nterface::Storage.root_dir, "{public,app/assets}", "javascripts", "**","*.{js,coffee}"))
191
+ end
192
+ end
@@ -0,0 +1,35 @@
1
+ class RailsI18nterface::Log
2
+ attr_accessor :from_locale, :to_locale, :keys
3
+
4
+ def initialize(from_locale, to_locale, keys)
5
+ self.from_locale = from_locale
6
+ self.to_locale = to_locale
7
+ self.keys = keys
8
+ end
9
+
10
+ def write_to_file
11
+ current_texts = File.exists?(file_path) ? file.read : {}
12
+ current_texts.merge!(from_texts)
13
+ file.write(current_texts)
14
+ end
15
+
16
+ def read
17
+ file.read
18
+ end
19
+
20
+ private
21
+ def file
22
+ @file ||= RailsI18nterface::File.new(file_path)
23
+ end
24
+
25
+ def from_texts
26
+ RailsI18nterface::File.deep_stringify_keys(RailsI18nterface::Keys.to_deep_hash(keys.inject({}) do |hash, key|
27
+ hash[key] = I18n.backend.send(:lookup, from_locale, key)
28
+ hash
29
+ end))
30
+ end
31
+
32
+ def file_path
33
+ File.join(Rails.root, "config", "locales", "log", "from_#{from_locale}_to_#{to_locale}.yml")
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ class RailsI18nterface::Storage
2
+ attr_accessor :locale
3
+
4
+ def initialize(locale)
5
+ self.locale = locale.to_sym
6
+ end
7
+
8
+ def write_to_file
9
+ RailsI18nterface::File.new(file_path).write(keys)
10
+ end
11
+
12
+ def self.file_paths(locale)
13
+ Dir.glob(File.join(root_dir, "config", "locales", "**","#{locale}.yml"))
14
+ end
15
+
16
+ def self.root_dir
17
+ Rails.root
18
+ end
19
+
20
+ private
21
+
22
+ def keys
23
+ {locale => I18n.backend.send(:translations)[locale]}
24
+ end
25
+
26
+ def file_path
27
+ File.join(self.class.root_dir, "config", "locales", "#{locale}.yml")
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module RailsI18nterface
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :translate do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,135 @@
1
+ require 'spec_helper'
2
+
3
+ describe RailsI18nterface::TranslateController do
4
+ describe "index" do
5
+ before(:each) do
6
+ controller.stub!(:per_page).and_return(1)
7
+ I18n.backend.stub!(:translations).and_return(i18n_translations)
8
+ I18n.backend.instance_eval { @initialized = true }
9
+ keys = mock(:keys)
10
+ keys.stub!(:i18n_keys).and_return(['vendor.foobar'])
11
+ RailsI18nterface::Keys.should_receive(:new).and_return(keys)
12
+ RailsI18nterface::Keys.should_receive(:files).and_return(files)
13
+ I18n.stub!(:valid_locales).and_return([:en, :sv])
14
+ I18n.stub!(:default_locale).and_return(:sv)
15
+ end
16
+
17
+ it "shows sorted paginated keys from the translate from locale and extracted keys by default" do
18
+ get_page :index, use_route: 'rails-i18nterface'
19
+ assigns(:from_locale).should == :sv
20
+ assigns(:to_locale).should == :en
21
+ assigns(:files).should == files
22
+ assigns(:keys).sort.should == ['articles.new.page_title', 'home.page_title', 'vendor.foobar']
23
+ assigns(:paginated_keys).should == ['articles.new.page_title']
24
+ end
25
+
26
+ it "can be paginated with the page param" do
27
+ get_page :index, :page => 2, use_route: 'rails-i18nterface'
28
+ assigns(:files).should == files
29
+ assigns(:paginated_keys).should == ['home.page_title']
30
+ end
31
+
32
+ it "accepts a key_pattern param with key_type=starts_with" do
33
+ get_page :index, :key_pattern => 'articles', :key_type => 'starts_with', use_route: 'rails-i18nterface'
34
+ assigns(:files).should == files
35
+ assigns(:paginated_keys).should == ['articles.new.page_title']
36
+ assigns(:total_entries).should == 1
37
+ end
38
+
39
+ it "accepts a key_pattern param with key_type=contains" do
40
+ get_page :index, :key_pattern => 'page_', :key_type => 'contains', use_route: 'rails-i18nterface'
41
+ assigns(:files).should == files
42
+ assigns(:total_entries).should == 2
43
+ assigns(:paginated_keys).should == ['articles.new.page_title']
44
+ end
45
+
46
+ it "accepts a filter=untranslated param" do
47
+ get_page :index, :filter => 'untranslated', use_route: 'rails-i18nterface'
48
+ assigns(:total_entries).should == 2
49
+ assigns(:paginated_keys).should == ['articles.new.page_title']
50
+ end
51
+
52
+ it "accepts a filter=translated param" do
53
+ get_page :index, :filter => 'translated', use_route: 'rails-i18nterface'
54
+ assigns(:total_entries).should == 1
55
+ assigns(:paginated_keys).should == ['vendor.foobar']
56
+ end
57
+
58
+ it "accepts a filter=changed param" do
59
+ log = mock(:log)
60
+ old_translations = {:home => {:page_title => "Skapar ny artikel"}}
61
+ log.should_receive(:read).and_return(RailsI18nterface::File.deep_stringify_keys(old_translations))
62
+ RailsI18nterface::Log.should_receive(:new).with(:sv, :en, {}).and_return(log)
63
+ get_page :index, :filter => 'changed', use_route: 'rails-i18nterface'
64
+ assigns(:total_entries).should == 1
65
+ assigns(:keys).should == ["home.page_title"]
66
+ end
67
+
68
+ def i18n_translations
69
+ HashWithIndifferentAccess.new({
70
+ :en => {
71
+ :vendor => {
72
+ :foobar => "Foo Baar"
73
+ }
74
+ },
75
+ :sv => {
76
+ :articles => {
77
+ :new => {
78
+ :page_title => "Skapa ny artikel"
79
+ }
80
+ },
81
+ :home => {
82
+ :page_title => "Valkommen till I18n"
83
+ },
84
+ :vendor => {
85
+ :foobar => "Fobar"
86
+ }
87
+ }
88
+ })
89
+ end
90
+
91
+ def files
92
+ HashWithIndifferentAccess.new({
93
+ :'home.page_title' => ["app/views/home/index.rhtml"],
94
+ :'general.back' => ["app/views/articles/new.rhtml", "app/views/categories/new.rhtml"],
95
+ :'articles.new.page_title' => ["app/views/articles/new.rhtml"]
96
+ })
97
+ end
98
+ end
99
+
100
+ describe "translate" do
101
+ it "should store translations to I18n backend and then write them to a YAML file" do
102
+ session[:from_locale] = :sv
103
+ session[:to_locale] = :en
104
+ translations = {
105
+ :articles => {
106
+ :new => {
107
+ :title => "New Article"
108
+ }
109
+ },
110
+ :category => "Category"
111
+ }
112
+ key_param = {'articles.new.title' => "New Article", "category" => "Category"}
113
+ #I18n.backend.should_receive(:store_translations).with(:en, translations)
114
+ storage = mock(:storage)
115
+ storage.should_receive(:write_to_file)
116
+ RailsI18nterface::Storage.should_receive(:new).with(:en).and_return(storage)
117
+ log = mock(:log)
118
+ log.should_receive(:write_to_file)
119
+ RailsI18nterface::Log.should_receive(:new).with(:sv, :en, key_param.keys).and_return(log)
120
+ RailsI18nterface::Storage.stub!(:root_dir).and_return(i18n_files_dir)
121
+ put :update, key: key_param, version: 1, use_route: 'rails-i18nterface'
122
+ response.should be_redirect
123
+ end
124
+ end
125
+
126
+ def get_page(*args)
127
+ get(*args, use_route: 'rails-i18nterface')
128
+ response.should be_success
129
+ end
130
+
131
+ def i18n_files_dir
132
+ File.expand_path(File.join("..", "..", "..", "spec", "internal"), __FILE__)
133
+ end
134
+
135
+ end