translate-rails3 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.
- data/MIT-LICENSE +20 -0
- data/README +62 -0
- data/Rakefile +33 -0
- data/VERSION +1 -0
- data/app/controllers/translate_controller.rb +165 -0
- data/app/helpers/translate_helper.rb +45 -0
- data/app/views/layouts/translate.html.erb +359 -0
- data/app/views/translate/_pagination.html.erb +24 -0
- data/app/views/translate/index.html.erb +114 -0
- data/config/routes.rb +5 -0
- data/init.rb +1 -0
- data/lib/tasks/translate.rake +178 -0
- data/lib/translate.rb +8 -0
- data/lib/translate/file.rb +35 -0
- data/lib/translate/keys.rb +152 -0
- data/lib/translate/log.rb +35 -0
- data/lib/translate/routes.rb +11 -0
- data/lib/translate/storage.rb +28 -0
- data/spec/controllers/translate_controller_spec.rb +130 -0
- data/spec/file_spec.rb +54 -0
- data/spec/files/translate/app/models/article.rb +12 -0
- data/spec/files/translate/app/views/category.erb +1 -0
- data/spec/files/translate/app/views/category.html +1 -0
- data/spec/files/translate/app/views/category.html.erb +1 -0
- data/spec/files/translate/app/views/category.rhtml +5 -0
- data/spec/files/translate/public/javascripts/application.js +1 -0
- data/spec/keys_spec.rb +179 -0
- data/spec/log_spec.rb +47 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/storage_spec.rb +33 -0
- data/translate-rails3.gemspec +77 -0
- metadata +118 -0
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            <% 
         | 
| 2 | 
            +
              # Expects locals:
         | 
| 3 | 
            +
              #
         | 
| 4 | 
            +
              # total_entries
         | 
| 5 | 
            +
              # per_page
         | 
| 6 | 
            +
             | 
| 7 | 
            +
              n_pages = total_entries/per_page + (total_entries % per_page > 0 ? 1 : 0)
         | 
| 8 | 
            +
              current_page = (params[:page] || 1).to_i
         | 
| 9 | 
            +
            %>
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            <% if n_pages > 1 %>
         | 
| 12 | 
            +
            <h2>Pages:</h2>
         | 
| 13 | 
            +
            <div class="clearfix">
         | 
| 14 | 
            +
            	<ul class="paging">
         | 
| 15 | 
            +
            	  <% (1..n_pages).each do |page_number| %>
         | 
| 16 | 
            +
            	    <% if current_page == page_number %>
         | 
| 17 | 
            +
            	      <li class="selected"><%= link_to(page_number, params.merge(:page => page_number), :title => "Page #{page_number}" ) %></li>
         | 
| 18 | 
            +
            	    <% else %>
         | 
| 19 | 
            +
            	      <li><%= link_to(page_number, params.merge(:page => page_number), :title => "Page #{page_number}") %></li>
         | 
| 20 | 
            +
            	    <% end %>
         | 
| 21 | 
            +
            	  <% end %>
         | 
| 22 | 
            +
            	</ul>
         | 
| 23 | 
            +
            </div>
         | 
| 24 | 
            +
            <% end %>
         | 
| @@ -0,0 +1,114 @@ | |
| 1 | 
            +
            <%
         | 
| 2 | 
            +
              @page_title = "Translate"
         | 
| 3 | 
            +
              show_filters = ["all", "untranslated", "translated"]
         | 
| 4 | 
            +
              show_filters << "changed" if @from_locale != @to_locale
         | 
| 5 | 
            +
            %>
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            <fieldset>
         | 
| 8 | 
            +
            	<legend>Search filter</legend>
         | 
| 9 | 
            +
            	<div id="show-sort">
         | 
| 10 | 
            +
            		<p>
         | 
| 11 | 
            +
            		 <label>Show:</label> <%=  raw simple_filter(show_filters) %>
         | 
| 12 | 
            +
            		</p>
         | 
| 13 | 
            +
            		<p>
         | 
| 14 | 
            +
            		 <label>Sort by:</label> <%= raw simple_filter(["key", "text"], 'sort_by') %>
         | 
| 15 | 
            +
            		</p>
         | 
| 16 | 
            +
            	</div>
         | 
| 17 | 
            +
              <%= form_tag(params, :method => :get) do %>
         | 
| 18 | 
            +
              <div id="languages">
         | 
| 19 | 
            +
               <p>
         | 
| 20 | 
            +
                 <%= hidden_field_tag(:filter, params[:filter]) %>
         | 
| 21 | 
            +
                 <%= hidden_field_tag(:sort_by, params[:sort_by]) %>
         | 
| 22 | 
            +
                 <label>Translate from</label>
         | 
| 23 | 
            +
                 <%= select_tag(:from_locale, options_for_select(I18n.available_locales, @from_locale.to_sym)) %> <span>to</span>
         | 
| 24 | 
            +
                 <%= select_tag(:to_locale, options_for_select(I18n.available_locales, @to_locale.to_sym)) %>
         | 
| 25 | 
            +
                 <%= submit_tag "Display" %>
         | 
| 26 | 
            +
               </p>
         | 
| 27 | 
            +
              </div>
         | 
| 28 | 
            +
              <div id="filter-pattern">
         | 
| 29 | 
            +
              <p>
         | 
| 30 | 
            +
                 <label for="key_pattern_value">Key</label>
         | 
| 31 | 
            +
                 <%= select_tag(:key_type, options_for_select([["contains", 'contains'], ["starts with", 'starts_with']], params[:key_type])) %>
         | 
| 32 | 
            +
                 <%= text_field_tag(:key_pattern, params[:key_pattern], :size => 50, :id => "key_pattern_value", :class => "text-default") %>
         | 
| 33 | 
            +
               </p>
         | 
| 34 | 
            +
               <p>
         | 
| 35 | 
            +
                 <label for="text_pattern_value">Text</label>
         | 
| 36 | 
            +
                 <%= select_tag(:text_type, options_for_select(['contains', 'equals'], params[:text_type])) %>
         | 
| 37 | 
            +
                 <%= text_field_tag(:text_pattern, params[:text_pattern], :size => 50, :id => "text_pattern_value", :class => "text-default") %>	     
         | 
| 38 | 
            +
               </p>
         | 
| 39 | 
            +
               <p>
         | 
| 40 | 
            +
                <%= submit_tag "Search" %>
         | 
| 41 | 
            +
                <%= link_to "clear", params.merge({:text_pattern => nil, :key_pattern => nil}) %>
         | 
| 42 | 
            +
                </p>
         | 
| 43 | 
            +
              </div>
         | 
| 44 | 
            +
              <% end %>
         | 
| 45 | 
            +
            	<p class="hits">
         | 
| 46 | 
            +
            	  Found <strong><%= @total_entries %></strong> messages
         | 
| 47 | 
            +
            	</p>
         | 
| 48 | 
            +
            	<p>
         | 
| 49 | 
            +
            	 <%= link_to "Reload messages", translate_reload_path %>
         | 
| 50 | 
            +
            	</p>
         | 
| 51 | 
            +
            </fieldset>
         | 
| 52 | 
            +
             | 
| 53 | 
            +
             | 
| 54 | 
            +
            <div class="paging">
         | 
| 55 | 
            +
              <%= render :partial => 'pagination', :locals => {:total_entries => @total_entries, :per_page => per_page} %>
         | 
| 56 | 
            +
            </div>
         | 
| 57 | 
            +
             | 
| 58 | 
            +
            <% if @total_entries > 0 %>
         | 
| 59 | 
            +
            <%= form_tag(translate_path) do %>
         | 
| 60 | 
            +
              <div>
         | 
| 61 | 
            +
                  <%= hidden_field_tag(:filter, params[:filter], :id => "hid_filter") %>
         | 
| 62 | 
            +
                  <%= hidden_field_tag(:sort_by, params[:sort_by], :id => "hid_sort_by") %>
         | 
| 63 | 
            +
                  <%= hidden_field_tag(:key_type, params[:key_type], :id => "hid_key_type") %>
         | 
| 64 | 
            +
                  <%= hidden_field_tag(:key_pattern, params[:key_pattern], :id => "hid_key_pattern") %>
         | 
| 65 | 
            +
                  <%= hidden_field_tag(:text_type, params[:text_type], :id => "hid_text_type") %>
         | 
| 66 | 
            +
                  <%= hidden_field_tag(:text_pattern, params[:text_pattern], :id => "hid_text_pattern") %>
         | 
| 67 | 
            +
              </div>
         | 
| 68 | 
            +
              <div class="translations">
         | 
| 69 | 
            +
            	  <h2>Translations from <%= @from_locale %> to <%= @to_locale %></h2>
         | 
| 70 | 
            +
                <p class="translate">
         | 
| 71 | 
            +
            	    <%= submit_tag "Save Translations" %>
         | 
| 72 | 
            +
            	  </p>
         | 
| 73 | 
            +
            	  <% @paginated_keys.each do |key| 
         | 
| 74 | 
            +
            	      from_text = lookup(@from_locale, key)
         | 
| 75 | 
            +
            	      to_text = lookup(@to_locale, key)
         | 
| 76 | 
            +
            	      line_size = 100
         | 
| 77 | 
            +
            	      n_lines = n_lines(from_text, line_size)
         | 
| 78 | 
            +
            	      field_name = "key[#{key}]"
         | 
| 79 | 
            +
                %>
         | 
| 80 | 
            +
            	  <div class="translation">    
         | 
| 81 | 
            +
            		<% if from_text.present? %>
         | 
| 82 | 
            +
            		  <p class="translation-text">
         | 
| 83 | 
            +
            				<%= simple_format(h(from_text)) %>
         | 
| 84 | 
            +
            			</p>
         | 
| 85 | 
            +
            		<% end %>
         | 
| 86 | 
            +
            		<p class="edit-form">
         | 
| 87 | 
            +
            		<% if n_lines > 1 %>
         | 
| 88 | 
            +
            			<%= text_area_tag(field_name, to_text, :size => "#{line_size}x#{n_lines}", :id => key) %>
         | 
| 89 | 
            +
            		<% else %>
         | 
| 90 | 
            +
            			<%= text_field_tag(field_name, to_text, :size => line_size, :id => key) %>
         | 
| 91 | 
            +
            		<% end %>
         | 
| 92 | 
            +
            		</p>
         | 
| 93 | 
            +
            		<p>
         | 
| 94 | 
            +
            		  <em>
         | 
| 95 | 
            +
            			  <%= link_to_function 'Auto Translate', "getGoogleTranslation('#{key}', \"#{escape_javascript(from_text)}\", '#{@from_locale}', '#{@to_locale}')", :style => 'padding: 0; margin: 0;' %>
         | 
| 96 | 
            +
            			  <br/>
         | 
| 97 | 
            +
            			  <strong>Key:</strong><%=h key %><br/>
         | 
| 98 | 
            +
            			  <% if @files[key] %>
         | 
| 99 | 
            +
            				  <strong>File:</strong><%= @files[key].join("<br/>") %>
         | 
| 100 | 
            +
            			    <% end %>
         | 
| 101 | 
            +
            			</em>
         | 
| 102 | 
            +
            		</p>
         | 
| 103 | 
            +
            	</div>  
         | 
| 104 | 
            +
            <% end %>
         | 
| 105 | 
            +
              <p class="translate">
         | 
| 106 | 
            +
                <%= submit_tag "Save Translations" %>
         | 
| 107 | 
            +
            	</p>
         | 
| 108 | 
            +
            </div>
         | 
| 109 | 
            +
            <% end %>
         | 
| 110 | 
            +
            <% end %>
         | 
| 111 | 
            +
             | 
| 112 | 
            +
            <div class="paging">
         | 
| 113 | 
            +
              <%= render :partial => 'pagination', :locals => {:total_entries => @total_entries, :per_page => per_page} %>
         | 
| 114 | 
            +
            </div>
         | 
    
        data/config/routes.rb
    ADDED
    
    | @@ -0,0 +1,5 @@ | |
| 1 | 
            +
            Rails.application.routes.draw do
         | 
| 2 | 
            +
              match 'translate' => 'translate#index', :as => :translate_list
         | 
| 3 | 
            +
              match 'translate/translate' => 'translate#translate', :as => :translate
         | 
| 4 | 
            +
              match 'translate/reload' => 'translate#reload', :as => :translate_reload
         | 
| 5 | 
            +
            end if Rails.env.development?
         | 
    
        data/init.rb
    ADDED
    
    | @@ -0,0 +1 @@ | |
| 1 | 
            +
            require 'translate'
         | 
| @@ -0,0 +1,178 @@ | |
| 1 | 
            +
            require 'yaml'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class Hash
         | 
| 4 | 
            +
              def deep_merge(other)
         | 
| 5 | 
            +
                # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
         | 
| 6 | 
            +
                merger = proc { |key, v1, v2| (Hash === v1 && Hash === v2) ? v1.merge(v2, &merger) : v2 }
         | 
| 7 | 
            +
                merge(other, &merger)
         | 
| 8 | 
            +
              end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def set(keys, value)
         | 
| 11 | 
            +
                key = keys.shift
         | 
| 12 | 
            +
                if keys.empty?
         | 
| 13 | 
            +
                  self[key] = value
         | 
| 14 | 
            +
                else
         | 
| 15 | 
            +
                  self[key] ||= {}
         | 
| 16 | 
            +
                  self[key].set keys, value
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              if ENV['SORT']
         | 
| 21 | 
            +
                # copy of ruby's to_yaml method, prepending sort.
         | 
| 22 | 
            +
                # before each so we get an ordered yaml file
         | 
| 23 | 
            +
                def to_yaml( opts = {} )
         | 
| 24 | 
            +
                  YAML::quick_emit( self, opts ) do |out|
         | 
| 25 | 
            +
                    out.map( taguri, to_yaml_style ) do |map|
         | 
| 26 | 
            +
                      sort.each do |k, v| #<- Adding sort.
         | 
| 27 | 
            +
                        map.add( k, v )
         | 
| 28 | 
            +
                      end
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
            namespace :translate do
         | 
| 36 | 
            +
              desc "Show untranslated keys for locale LOCALE"
         | 
| 37 | 
            +
              task :untranslated => :environment do
         | 
| 38 | 
            +
                from_locale = I18n.default_locale
         | 
| 39 | 
            +
                untranslated = Translate::Keys.new.untranslated_keys
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                messages = []
         | 
| 42 | 
            +
                untranslated.each do |locale, keys|
         | 
| 43 | 
            +
                  keys.each do |key|
         | 
| 44 | 
            +
                    from_text = I18n.backend.send(:lookup, from_locale, key)
         | 
| 45 | 
            +
                    messages << "#{locale}.#{key} (#{from_locale}.#{key}='#{from_text}')"
         | 
| 46 | 
            +
                  end
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
                  
         | 
| 49 | 
            +
                if messages.present?
         | 
| 50 | 
            +
                  messages.each { |m| puts m }
         | 
| 51 | 
            +
                else
         | 
| 52 | 
            +
                  puts "No untranslated keys"
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
              
         | 
| 56 | 
            +
              desc "Show I18n keys that are missing in the config/locales/default_locale.yml YAML file"
         | 
| 57 | 
            +
              task :missing => :environment do
         | 
| 58 | 
            +
                missing = Translate::Keys.new.missing_keys.inject([]) do |keys, (key, filename)|
         | 
| 59 | 
            +
                  keys << "#{key} in \t  #{filename} is missing"
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
                puts missing.present? ? missing.join("\n") : "No missing translations in the default locale file"
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              desc "Remove all translation texts that are no longer present in the locale they were translated from"
         | 
| 65 | 
            +
              task :remove_obsolete_keys => :environment do
         | 
| 66 | 
            +
                I18n.backend.send(:init_translations)
         | 
| 67 | 
            +
                master_locale = ENV['LOCALE'] || I18n.default_locale
         | 
| 68 | 
            +
                Translate::Keys.translated_locales.each do |locale|
         | 
| 69 | 
            +
                  texts = {}
         | 
| 70 | 
            +
                  Translate::Keys.new.i18n_keys(locale).each do |key|
         | 
| 71 | 
            +
                    if I18n.backend.send(:lookup, master_locale, key).to_s.present?
         | 
| 72 | 
            +
                      texts[key] = I18n.backend.send(:lookup, locale, key)
         | 
| 73 | 
            +
                    end
         | 
| 74 | 
            +
                  end
         | 
| 75 | 
            +
                  I18n.backend.send(:translations)[locale] = nil # Clear out all current translations
         | 
| 76 | 
            +
                  I18n.backend.store_translations(locale, Translate::Keys.to_deep_hash(texts))
         | 
| 77 | 
            +
                  Translate::Storage.new(locale).write_to_file      
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
              end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
              desc "Merge I18n keys from log/translations.yml into config/locales/*.yml (for use with the Rails I18n TextMate bundle)"
         | 
| 82 | 
            +
              task :merge_keys => :environment do
         | 
| 83 | 
            +
                I18n.backend.send(:init_translations)
         | 
| 84 | 
            +
                new_translations = YAML::load(IO.read(File.join(Rails.root, "log", "translations.yml")))
         | 
| 85 | 
            +
                raise("Can only merge in translations in single locale") if new_translations.keys.size > 1
         | 
| 86 | 
            +
                locale = new_translations.keys.first
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                overwrites = false
         | 
| 89 | 
            +
                Translate::Keys.to_shallow_hash(new_translations[locale]).keys.each do |key|
         | 
| 90 | 
            +
                  new_text = key.split(".").inject(new_translations[locale]) { |hash, sub_key| hash[sub_key] }
         | 
| 91 | 
            +
                  existing_text = I18n.backend.send(:lookup, locale.to_sym, key)
         | 
| 92 | 
            +
                  if existing_text && new_text != existing_text        
         | 
| 93 | 
            +
                    puts "ERROR: key #{key} already exists with text '#{existing_text.inspect}' and would be overwritten by new text '#{new_text}'. " +
         | 
| 94 | 
            +
                      "Set environment variable OVERWRITE=1 if you really want to do this."
         | 
| 95 | 
            +
                    overwrites = true
         | 
| 96 | 
            +
                  end
         | 
| 97 | 
            +
                end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
                if !overwrites || ENV['OVERWRITE']
         | 
| 100 | 
            +
                  I18n.backend.store_translations(locale, new_translations[locale])
         | 
| 101 | 
            +
                  Translate::Storage.new(locale).write_to_file
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
              end
         | 
| 104 | 
            +
              
         | 
| 105 | 
            +
              desc "Apply Google translate to auto translate all texts in locale ENV['FROM'] to locale ENV['TO']"
         | 
| 106 | 
            +
              task :google => :environment do
         | 
| 107 | 
            +
                raise "Please specify FROM and TO locales as environment variables" if ENV['FROM'].blank? || ENV['TO'].blank?
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                # Depends on httparty gem
         | 
| 110 | 
            +
                # http://www.robbyonrails.com/articles/2009/03/16/httparty-goes-foreign
         | 
| 111 | 
            +
                class GoogleApi
         | 
| 112 | 
            +
                  include HTTParty
         | 
| 113 | 
            +
                  base_uri 'ajax.googleapis.com'
         | 
| 114 | 
            +
                  def self.translate(string, to, from)
         | 
| 115 | 
            +
                    tries = 0
         | 
| 116 | 
            +
                    begin
         | 
| 117 | 
            +
                      get("/ajax/services/language/translate",
         | 
| 118 | 
            +
                        :query => {:langpair => "#{from}|#{to}", :q => string, :v => 1.0},
         | 
| 119 | 
            +
                        :format => :json)
         | 
| 120 | 
            +
                    rescue 
         | 
| 121 | 
            +
                      tries += 1
         | 
| 122 | 
            +
                      puts("SLEEPING - retrying in 5...")
         | 
| 123 | 
            +
                      sleep(5)
         | 
| 124 | 
            +
                      retry if tries < 10
         | 
| 125 | 
            +
                    end
         | 
| 126 | 
            +
                  end
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                I18n.backend.send(:init_translations)
         | 
| 130 | 
            +
             | 
| 131 | 
            +
                start_at = Time.now
         | 
| 132 | 
            +
                translations = {}
         | 
| 133 | 
            +
                Translate::Keys.new.i18n_keys(ENV['FROM']).each do |key|
         | 
| 134 | 
            +
                  from_text = I18n.backend.send(:lookup, ENV['FROM'], key).to_s
         | 
| 135 | 
            +
                  to_text = I18n.backend.send(:lookup, ENV['TO'], key)
         | 
| 136 | 
            +
                  if !from_text.blank? && to_text.blank?
         | 
| 137 | 
            +
                    print "#{key}: '#{from_text[0, 40]}' => "
         | 
| 138 | 
            +
                    if !translations[from_text]
         | 
| 139 | 
            +
                      response = GoogleApi.translate(from_text, ENV['TO'], ENV['FROM'])
         | 
| 140 | 
            +
                      translations[from_text] = response["responseData"] && response["responseData"]["translatedText"]
         | 
| 141 | 
            +
                    end
         | 
| 142 | 
            +
                    if !(translation = translations[from_text]).blank?
         | 
| 143 | 
            +
                      translation.gsub!(/\(\(([a-z_.]+)\)\)/i, '{{\1}}')
         | 
| 144 | 
            +
                      # Google translate sometimes replaces {{foobar}} with (()) foobar. We skip these
         | 
| 145 | 
            +
                      if translation !~ /\(\(\)\)/
         | 
| 146 | 
            +
                        puts "'#{translation[0, 40]}'"
         | 
| 147 | 
            +
                        I18n.backend.store_translations(ENV['TO'].to_sym, Translate::Keys.to_deep_hash({key => translation}))
         | 
| 148 | 
            +
                      else
         | 
| 149 | 
            +
                        puts "SKIPPING since interpolations were messed up: '#{translation[0,40]}'"
         | 
| 150 | 
            +
                      end
         | 
| 151 | 
            +
                    else
         | 
| 152 | 
            +
                      puts "NO TRANSLATION - #{response.inspect}"
         | 
| 153 | 
            +
                    end
         | 
| 154 | 
            +
                  end
         | 
| 155 | 
            +
                end
         | 
| 156 | 
            +
                
         | 
| 157 | 
            +
                puts "\nTime elapsed: #{(((Time.now - start_at) / 60) * 10).to_i / 10.to_f} minutes"    
         | 
| 158 | 
            +
                Translate::Storage.new(ENV['TO'].to_sym).write_to_file
         | 
| 159 | 
            +
              end
         | 
| 160 | 
            +
             | 
| 161 | 
            +
              desc "List keys that have changed I18n texts between YAML file ENV['FROM_FILE'] and YAML file ENV['TO_FILE']. Set ENV['VERBOSE'] to see changes"
         | 
| 162 | 
            +
              task :changed => :environment do
         | 
| 163 | 
            +
                from_hash = Translate::Keys.to_shallow_hash(Translate::File.new(ENV['FROM_FILE']).read)
         | 
| 164 | 
            +
                to_hash = Translate::Keys.to_shallow_hash(Translate::File.new(ENV['TO_FILE']).read)
         | 
| 165 | 
            +
                from_hash.each do |key, from_value|
         | 
| 166 | 
            +
                  if (to_value = to_hash[key]) && to_value != from_value
         | 
| 167 | 
            +
                    key_without_locale = key[/^[^.]+\.(.+)$/, 1]
         | 
| 168 | 
            +
                    if ENV['VERBOSE']
         | 
| 169 | 
            +
                      puts "KEY: #{key_without_locale}"
         | 
| 170 | 
            +
                      puts "FROM VALUE: '#{from_value}'"
         | 
| 171 | 
            +
                      puts "TO VALUE: '#{to_value}'"
         | 
| 172 | 
            +
                    else
         | 
| 173 | 
            +
                      puts key_without_locale
         | 
| 174 | 
            +
                    end
         | 
| 175 | 
            +
                  end      
         | 
| 176 | 
            +
                end
         | 
| 177 | 
            +
              end
         | 
| 178 | 
            +
            end
         | 
    
        data/lib/translate.rb
    ADDED
    
    
| @@ -0,0 +1,35 @@ | |
| 1 | 
            +
            require 'fileutils'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class Translate::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(Translate::File.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,152 @@ | |
| 1 | 
            +
            require 'pathname'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class Translate::Keys
         | 
| 4 | 
            +
              # Allows keys extracted from lookups in files to be cached
         | 
| 5 | 
            +
              def self.files
         | 
| 6 | 
            +
                @@files ||= Translate::Keys.new.files
         | 
| 7 | 
            +
              end
         | 
| 8 | 
            +
              
         | 
| 9 | 
            +
              # Allows flushing of the files cache
         | 
| 10 | 
            +
              def self.files=(files)
         | 
| 11 | 
            +
                @@files = files
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
              
         | 
| 14 | 
            +
              def files
         | 
| 15 | 
            +
                @files ||= extract_files
         | 
| 16 | 
            +
              end  
         | 
| 17 | 
            +
              alias_method :to_hash, :files
         | 
| 18 | 
            +
              
         | 
| 19 | 
            +
              def keys
         | 
| 20 | 
            +
                files.keys
         | 
| 21 | 
            +
              end
         | 
| 22 | 
            +
              alias_method :to_a, :keys
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              def i18n_keys(locale)
         | 
| 25 | 
            +
                I18n.backend.send(:init_translations) unless I18n.backend.initialized?
         | 
| 26 | 
            +
                Translate::Keys.to_shallow_hash(I18n.backend.send(:translations)[locale.to_sym]).keys.sort
         | 
| 27 | 
            +
              end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
              def untranslated_keys
         | 
| 30 | 
            +
                Translate::Keys.translated_locales.inject({}) do |missing, locale|
         | 
| 31 | 
            +
                  missing[locale] = i18n_keys(I18n.default_locale).map do |key|
         | 
| 32 | 
            +
                    I18n.backend.send(:lookup, locale, key).nil? ? key : nil
         | 
| 33 | 
            +
                  end.compact
         | 
| 34 | 
            +
                  missing
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              def missing_keys
         | 
| 39 | 
            +
                locale = I18n.default_locale; yaml_keys = {}
         | 
| 40 | 
            +
                yaml_keys = Translate::Storage.file_paths(locale).inject({}) do |keys, path|
         | 
| 41 | 
            +
                  keys = keys.deep_merge(Translate::File.new(path).read[locale.to_s])
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
                files.reject { |key, file| self.class.contains_key?(yaml_keys, key) }
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              def self.translated_locales
         | 
| 47 | 
            +
                I18n.available_locales.reject { |locale| [:root, I18n.default_locale.to_sym].include?(locale) }        
         | 
| 48 | 
            +
              end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
              # Checks if a nested hash contains the keys in dot separated I18n key.
         | 
| 51 | 
            +
              #
         | 
| 52 | 
            +
              # Example:
         | 
| 53 | 
            +
              #
         | 
| 54 | 
            +
              # hash = {
         | 
| 55 | 
            +
              #   :foo => {
         | 
| 56 | 
            +
              #     :bar => {
         | 
| 57 | 
            +
              #       :baz => 1
         | 
| 58 | 
            +
              #     }
         | 
| 59 | 
            +
              #   }
         | 
| 60 | 
            +
              # }
         | 
| 61 | 
            +
              #
         | 
| 62 | 
            +
              # contains_key?("foo", key) # => true
         | 
| 63 | 
            +
              # contains_key?("foo.bar", key) # => true
         | 
| 64 | 
            +
              # contains_key?("foo.bar.baz", key) # => true
         | 
| 65 | 
            +
              # contains_key?("foo.bar.baz.bla", key) # => false
         | 
| 66 | 
            +
              #
         | 
| 67 | 
            +
              def self.contains_key?(hash, key)
         | 
| 68 | 
            +
                keys = key.to_s.split(".")
         | 
| 69 | 
            +
                return false if keys.empty?
         | 
| 70 | 
            +
                !keys.inject(HashWithIndifferentAccess.new(hash)) do |memo, key|
         | 
| 71 | 
            +
                  memo.is_a?(Hash) ? memo.try(:[], key) : nil
         | 
| 72 | 
            +
                end.nil?
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
              
         | 
| 75 | 
            +
              # Convert something like:
         | 
| 76 | 
            +
              # 
         | 
| 77 | 
            +
              # {
         | 
| 78 | 
            +
              #  :pressrelease => {
         | 
| 79 | 
            +
              #    :label => {
         | 
| 80 | 
            +
              #      :one => "Pressmeddelande"
         | 
| 81 | 
            +
              #    }
         | 
| 82 | 
            +
              #   }
         | 
| 83 | 
            +
              # }
         | 
| 84 | 
            +
              # 
         | 
| 85 | 
            +
              # to:
         | 
| 86 | 
            +
              # 
         | 
| 87 | 
            +
              #  {'pressrelease.label.one' => "Pressmeddelande"}
         | 
| 88 | 
            +
              #
         | 
| 89 | 
            +
              def self.to_shallow_hash(hash)
         | 
| 90 | 
            +
                hash.inject({}) do |shallow_hash, (key, value)|
         | 
| 91 | 
            +
                  if value.is_a?(Hash)
         | 
| 92 | 
            +
                    to_shallow_hash(value).each do |sub_key, sub_value|
         | 
| 93 | 
            +
                      shallow_hash[[key, sub_key].join(".")] = sub_value
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
                  else
         | 
| 96 | 
            +
                    shallow_hash[key.to_s] = value
         | 
| 97 | 
            +
                  end
         | 
| 98 | 
            +
                  shallow_hash
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
              end
         | 
| 101 | 
            +
              
         | 
| 102 | 
            +
              # Convert something like:
         | 
| 103 | 
            +
              # 
         | 
| 104 | 
            +
              #  {'pressrelease.label.one' => "Pressmeddelande"}
         | 
| 105 | 
            +
              # 
         | 
| 106 | 
            +
              # to:
         | 
| 107 | 
            +
              # 
         | 
| 108 | 
            +
              # {
         | 
| 109 | 
            +
              #  :pressrelease => {
         | 
| 110 | 
            +
              #    :label => {
         | 
| 111 | 
            +
              #      :one => "Pressmeddelande"
         | 
| 112 | 
            +
              #    }
         | 
| 113 | 
            +
              #   }
         | 
| 114 | 
            +
              # }
         | 
| 115 | 
            +
              def self.to_deep_hash(hash)    
         | 
| 116 | 
            +
                hash.inject({}) do |deep_hash, (key, value)|
         | 
| 117 | 
            +
                  keys = key.to_s.split('.').reverse
         | 
| 118 | 
            +
                  leaf_key = keys.shift
         | 
| 119 | 
            +
                  key_hash = keys.inject({leaf_key.to_sym => value}) { |hash, key| {key.to_sym => hash} }
         | 
| 120 | 
            +
                  deep_merge!(deep_hash, key_hash)
         | 
| 121 | 
            +
                  deep_hash
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
              end
         | 
| 124 | 
            +
             | 
| 125 | 
            +
              # deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
         | 
| 126 | 
            +
              def self.deep_merge!(hash1, hash2)
         | 
| 127 | 
            +
                merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
         | 
| 128 | 
            +
                hash1.merge!(hash2, &merger)
         | 
| 129 | 
            +
              end
         | 
| 130 | 
            +
             | 
| 131 | 
            +
              private
         | 
| 132 | 
            +
             | 
| 133 | 
            +
              def extract_files
         | 
| 134 | 
            +
                files_to_scan.inject(HashWithIndifferentAccess.new) do |files, file|
         | 
| 135 | 
            +
                  IO.read(file).scan(i18n_lookup_pattern).flatten.map(&:to_sym).each do |key|
         | 
| 136 | 
            +
                    files[key] ||= []
         | 
| 137 | 
            +
                    path = Pathname.new(File.expand_path(file)).relative_path_from(Pathname.new(Rails.root)).to_s
         | 
| 138 | 
            +
                    files[key] << path if !files[key].include?(path)
         | 
| 139 | 
            +
                  end
         | 
| 140 | 
            +
                  files
         | 
| 141 | 
            +
                end
         | 
| 142 | 
            +
              end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
              def i18n_lookup_pattern
         | 
| 145 | 
            +
                /\b(?:I18n\.t|I18n\.translate|t)(?:\s|\():?'([a-z0-9_]+.[a-z0-9_.]+)'\)?/
         | 
| 146 | 
            +
              end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
              def files_to_scan
         | 
| 149 | 
            +
                Dir.glob(File.join(Translate::Storage.root_dir, "{app,config,lib}", "**","*.{rb,erb,rhtml}")) +
         | 
| 150 | 
            +
                  Dir.glob(File.join(Translate::Storage.root_dir, "public", "javascripts", "**","*.js"))
         | 
| 151 | 
            +
              end
         | 
| 152 | 
            +
            end
         |