rails-i18nterface 0.1.0

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