forgot to add these

This commit is contained in:
Reinier Balt 2012-04-05 22:21:28 +02:00
parent 86afd42148
commit fde64e0b3d
145 changed files with 9044 additions and 0 deletions

View file

@ -0,0 +1,20 @@
Copyright (c) 2009 [name of plugin creator]
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,63 @@
Translate
=========
This plugin provides a web interface for translating Rails I18n texts (requires Rails 2.2 or higher) from one locale to another. The plugin has been tested only with the simple I18n backend that ships with Rails. I18n texts are read from and written to YAML files under config/locales.
To translate to a new locale you need to add a YAML file for that locale that contains the locale as the top key and at least one translation.
Please note that there are certain I18n keys that map to Array objects rather than strings and those are currently not dealt with by the translation UI. This means that Rails built in keys such as date.day_names need to be translated manually directly in the YAML file.
To get the translation UI to write the YAML files in UTF8 you need to install the ya2yaml gem.
The translation UI finds all I18n keys by extracting them from I18n lookups in your application source code. In addition it adds all :en and default locale keys from the I18n backend.
- Updated: Each string in the UI now has an "Auto Translate" link which will send the original text to Google Translate and will input the returned translation into the form field for further clean up and review prior to saving.
Rake Tasks
=========
In addition to the web UI this plugin adds the following rake tasks:
translate:untranslated
translate:missing
translate:remove_obsolete_keys
translate:merge_keys
translate:google
translate:changed
The missing task shows you any I18n keys in your code that do not have translations in the YAML file for your default locale, i.e. config/locales/sv.yml.
The merge_keys task is supposed to be used in conjunction with Sven Fuch's Rails I18n TextMate bundle (http://github.com/svenfuchs/rails-i18n/tree/master). Texts and keys extracted with the TextMate bundle end up in the temporary file log/translations.yml. When you run the merge_keys rake task the keys are moved over to the corresponding I18n locale file, i.e. config/locales/sv.yml. The merge_keys task also checks for overwrites of existing keys by warning you that one of your extracted keys already exists with a different translation.
The google task is used for auto translating from one locale to another using Google Translate.
The changed rake task can show you between one YAML file to another which keys have had their texts changed.
Installation
=========
Obtain the source with:
./script/plugin install git://github.com/newsdesk/translate.git
To mount the plugin, add the following to your config/routes.rb file:
Translate::Routes.translation_ui(map) if RAILS_ENV != "production"
Now visit /translate in your web browser to start translating.
Dependencies
=========
- Rails 2.2 or higher
- The ya2yaml gem if you want your YAML files written in UTF8 encoding.
Authors
=========
- Peter Marklund (programming)
- Joakim Westerlund (web design)
Many thanks to http://newsdesk.se for sponsoring the development of this plugin.
Copyright (c) 2009 Peter Marklund, released under the MIT license

View file

@ -0,0 +1,11 @@
require 'rake'
require 'spec/rake/spectask'
desc 'Default: run specs.'
task :default => :spec
desc 'Run the specs'
Spec::Rake::SpecTask.new(:spec) do |t|
t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
t.spec_files = FileList['spec/**/*_spec.rb']
end

View file

@ -0,0 +1,8 @@
require 'translate'
# TODO: Use new method available_locales once Rails is upgraded, see:
# http://github.com/svenfuchs/i18n/commit/411f8fe7c8f3f89e9b6b921fa62ed66cb92f3af4
def I18n.valid_locales
I18n.backend.send(:init_translations) unless I18n.backend.initialized?
backend.send(:translations).keys.reject { |locale| locale == :root }
end

View file

@ -0,0 +1,8 @@
module Translate
end
require File.join(File.dirname(__FILE__), "translate_controller")
require File.join(File.dirname(__FILE__), "translate_helper")
Dir[File.join(File.dirname(__FILE__), "translate", "*.rb")].each do |file|
require file
end

View file

@ -0,0 +1,35 @@
require 'fileutils'
class Translate::File
attr_accessor :path
def initialize(path)
self.path = path
end
def write(keys)
FileUtils.mkdir_p File.dirname(path)
File.open(path, "w") do |file|
file.puts keys_to_yaml(Translate::File.deep_stringify_keys(keys))
end
end
def read
File.exists?(path) ? YAML::load(IO.read(path)) : {}
end
# Stringifying keys for prettier YAML
def self.deep_stringify_keys(hash)
hash.inject({}) { |result, (key, value)|
value = deep_stringify_keys(value) if value.is_a? Hash
result[(key.to_s rescue key) || key] = value
result
}
end
private
def keys_to_yaml(keys)
# Using ya2yaml, if available, for UTF8 support
keys.respond_to?(:ya2yaml) ? keys.ya2yaml(:escape_as_utf8 => true) : keys.to_yaml
end
end

View file

@ -0,0 +1,152 @@
require 'pathname'
class Translate::Keys
# Allows keys extracted from lookups in files to be cached
def self.files
@@files ||= Translate::Keys.new.files
end
# Allows flushing of the files cache
def self.files=(files)
@@files = files
end
def files
@files ||= extract_files
end
alias_method :to_hash, :files
def keys
files.keys
end
alias_method :to_a, :keys
def i18n_keys(locale)
I18n.backend.send(:init_translations) unless I18n.backend.initialized?
Translate::Keys.to_shallow_hash(I18n.backend.send(:translations)[locale.to_sym]).keys.sort
end
def untranslated_keys
Translate::Keys.translated_locales.inject({}) do |missing, locale|
missing[locale] = i18n_keys(I18n.default_locale).map do |key|
I18n.backend.send(:lookup, locale, key).nil? ? key : nil
end.compact
missing
end
end
def missing_keys
locale = I18n.default_locale; yaml_keys = {}
yaml_keys = Translate::Storage.file_paths(locale).inject({}) do |keys, path|
keys = keys.deep_merge(Translate::File.new(path).read[locale.to_s])
end
files.reject { |key, file| self.class.contains_key?(yaml_keys, key) }
end
def self.translated_locales
I18n.available_locales.reject { |locale| [:root, I18n.default_locale.to_sym].include?(locale) }
end
# Checks if a nested hash contains the keys in dot separated I18n key.
#
# Example:
#
# hash = {
# :foo => {
# :bar => {
# :baz => 1
# }
# }
# }
#
# contains_key?("foo", key) # => true
# contains_key?("foo.bar", key) # => true
# contains_key?("foo.bar.baz", key) # => true
# contains_key?("foo.bar.baz.bla", key) # => false
#
def self.contains_key?(hash, key)
keys = key.to_s.split(".")
return false if keys.empty?
!keys.inject(HashWithIndifferentAccess.new(hash)) do |memo, key|
memo.is_a?(Hash) ? memo.try(:[], key) : nil
end.nil?
end
# Convert something like:
#
# {
# :pressrelease => {
# :label => {
# :one => "Pressmeddelande"
# }
# }
# }
#
# to:
#
# {'pressrelease.label.one' => "Pressmeddelande"}
#
def self.to_shallow_hash(hash)
hash.inject({}) do |shallow_hash, (key, value)|
if value.is_a?(Hash)
to_shallow_hash(value).each do |sub_key, sub_value|
shallow_hash[[key, sub_key].join(".")] = sub_value
end
else
shallow_hash[key.to_s] = value
end
shallow_hash
end
end
# Convert something like:
#
# {'pressrelease.label.one' => "Pressmeddelande"}
#
# to:
#
# {
# :pressrelease => {
# :label => {
# :one => "Pressmeddelande"
# }
# }
# }
def self.to_deep_hash(hash)
hash.inject({}) do |deep_hash, (key, value)|
keys = key.to_s.split('.').reverse
leaf_key = keys.shift
key_hash = keys.inject({leaf_key.to_sym => value}) { |hash, key| {key.to_sym => hash} }
deep_merge!(deep_hash, key_hash)
deep_hash
end
end
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
def self.deep_merge!(hash1, hash2)
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
hash1.merge!(hash2, &merger)
end
private
def extract_files
files_to_scan.inject(HashWithIndifferentAccess.new) do |files, file|
IO.read(file).scan(i18n_lookup_pattern).flatten.map(&:to_sym).each do |key|
files[key] ||= []
path = Pathname.new(File.expand_path(file)).relative_path_from(Pathname.new(Rails.root)).to_s
files[key] << path if !files[key].include?(path)
end
files
end
end
def i18n_lookup_pattern
/\b(?:I18n\.t|I18n\.translate|t)(?:\s|\():?'([a-z0-9_]+.[a-z0-9_.]+)'\)?/
end
def files_to_scan
Dir.glob(File.join(Translate::Storage.root_dir, "{app,config,lib}", "**","*.{rb,erb,rhtml}")) +
Dir.glob(File.join(Translate::Storage.root_dir, "public", "javascripts", "**","*.js"))
end
end

View file

@ -0,0 +1,35 @@
class Translate::Log
attr_accessor :from_locale, :to_locale, :keys
def initialize(from_locale, to_locale, keys)
self.from_locale = from_locale
self.to_locale = to_locale
self.keys = keys
end
def write_to_file
current_texts = File.exists?(file_path) ? file.read : {}
current_texts.merge!(from_texts)
file.write(current_texts)
end
def read
file.read
end
private
def file
@file ||= Translate::File.new(file_path)
end
def from_texts
Translate::File.deep_stringify_keys(Translate::Keys.to_deep_hash(keys.inject({}) do |hash, key|
hash[key] = I18n.backend.send(:lookup, from_locale, key)
hash
end))
end
def file_path
File.join(Rails.root, "config", "locales", "log", "from_#{from_locale}_to_#{to_locale}.yml")
end
end

View file

@ -0,0 +1,11 @@
module Translate
class Routes
def self.translation_ui(map)
map.with_options(:controller => 'translate') do |t|
t.translate_list 'translate'
t.translate 'translate/translate', :action => 'translate'
t.translate_reload 'translate/reload', :action => 'reload'
end
end
end
end

View file

@ -0,0 +1,28 @@
class Translate::Storage
attr_accessor :locale
def initialize(locale)
self.locale = locale.to_sym
end
def write_to_file
Translate::File.new(file_path).write(keys)
end
def self.file_paths(locale)
Dir.glob(File.join(root_dir, "config", "locales", "**","#{locale}.yml"))
end
def self.root_dir
Rails.root
end
private
def keys
{locale => I18n.backend.send(:translations)[locale]}
end
def file_path
File.join(Translate::Storage.root_dir, "config", "locales", "#{locale}.yml")
end
end

View file

@ -0,0 +1,165 @@
class TranslateController < ActionController::Base
# It seems users with active_record_store may get a "no :secret given" error if we don't disable csrf protection,
skip_before_filter :verify_authenticity_token
prepend_view_path(File.join(File.dirname(__FILE__), "..", "views"))
layout 'translate'
before_filter :init_translations
before_filter :set_locale
def index
initialize_keys
filter_by_key_pattern
filter_by_text_pattern
filter_by_translated_or_changed
sort_keys
paginate_keys
@total_entries = @keys.size
end
def translate
I18n.backend.store_translations(@to_locale, Translate::Keys.to_deep_hash(params[:key]))
Translate::Storage.new(@to_locale).write_to_file
Translate::Log.new(@from_locale, @to_locale, params[:key].keys).write_to_file
force_init_translations # Force reload from YAML file
flash[:notice] = "Translations stored"
redirect_to params.slice(:filter, :sort_by, :key_type, :key_pattern, :text_type, :text_pattern).merge({:action => :index})
end
def reload
Translate::Keys.files = nil
redirect_to :action => 'index'
end
private
def initialize_keys
@files = Translate::Keys.files
@keys = (@files.keys.map(&:to_s) + Translate::Keys.new.i18n_keys(@from_locale)).uniq
@keys.reject! do |key|
from_text = lookup(@from_locale, key)
# When translating from one language to another, make sure there is a text to translate from.
# Always exclude non string translation objects as we don't support editing them in the UI.
(@from_locale != @to_locale && !from_text.present?) || (from_text.present? && !from_text.is_a?(String))
end
end
def lookup(locale, key)
I18n.backend.send(:lookup, locale, key)
end
helper_method :lookup
def filter_by_translated_or_changed
params[:filter] ||= 'all'
return if params[:filter] == 'all'
@keys.reject! do |key|
case params[:filter]
when 'untranslated'
lookup(@to_locale, key).present?
when 'translated'
lookup(@to_locale, key).blank?
when 'changed'
old_from_text(key).blank? || lookup(@from_locale, key) == old_from_text(key)
else
raise "Unknown filter '#{params[:filter]}'"
end
end
end
def filter_by_key_pattern
return if params[:key_pattern].blank?
@keys.reject! do |key|
case params[:key_type]
when "starts_with"
!key.starts_with?(params[:key_pattern])
when "contains"
key.index(params[:key_pattern]).nil?
else
raise "Unknown key_type '#{params[:key_type]}'"
end
end
end
def filter_by_text_pattern
return if params[:text_pattern].blank?
@keys.reject! do |key|
case params[:text_type]
when 'contains'
!lookup(@from_locale, key).present? || !lookup(@from_locale, key).to_s.downcase.index(params[:text_pattern].downcase)
when 'equals'
!lookup(@from_locale, key).present? || lookup(@from_locale, key).to_s.downcase != params[:text_pattern].downcase
else
raise "Unknown text_type '#{params[:text_type]}'"
end
end
end
def sort_keys
params[:sort_by] ||= "key"
case params[:sort_by]
when "key"
@keys.sort!
when "text"
@keys.sort! do |key1, key2|
if lookup(@from_locale, key1).present? && lookup(@from_locale, key2).present?
lookup(@from_locale, key1).to_s.downcase <=> lookup(@from_locale, key2).to_s.downcase
elsif lookup(@from_locale, key1).present?
-1
else
1
end
end
else
raise "Unknown sort_by '#{params[:sort_by]}'"
end
end
def paginate_keys
params[:page] ||= 1
@paginated_keys = @keys[offset, per_page]
end
def offset
(params[:page].to_i - 1) * per_page
end
def per_page
50
end
helper_method :per_page
def init_translations
I18n.backend.send(:init_translations) unless I18n.backend.initialized?
end
def force_init_translations
I18n.backend.send(:init_translations)
end
def default_locale
I18n.default_locale
end
def set_locale
session[:from_locale] ||= default_locale
session[:to_locale] ||= :en
session[:from_locale] = params[:from_locale] if params[:from_locale].present?
session[:to_locale] = params[:to_locale] if params[:to_locale].present?
@from_locale = session[:from_locale].to_sym
@to_locale = session[:to_locale].to_sym
end
def old_from_text(key)
return @old_from_text[key] if @old_from_text && @old_from_text[key]
@old_from_text = {}
text = key.split(".").inject(log_hash) do |hash, k|
hash ? hash[k] : nil
end
@old_from_text[key] = text
end
helper_method :old_from_text
def log_hash
@log_hash ||= Translate::Log.new(@from_locale, @to_locale, {}).read
end
end

View file

@ -0,0 +1,45 @@
module TranslateHelper
def simple_filter(labels, param_name = 'filter', selected_value = nil)
selected_value ||= params[param_name]
filter = []
labels.each do |item|
if item.is_a?(Array)
type, label = item
else
type = label = item
end
if type.to_s == selected_value.to_s
filter << "<i>#{label}</i>"
else
link_params = params.merge({param_name.to_s => type})
link_params.merge!({"page" => nil}) if param_name.to_s != "page"
filter << link_to(label, link_params)
end
end
filter.join(" | ")
end
def n_lines(text, line_size)
n_lines = 1
if text.present?
n_lines = text.split("\n").size
if n_lines == 1 && text.length > line_size
n_lines = text.length / line_size + 1
end
end
n_lines
end
def translate_javascript_includes
sources = []
if File.exists?(File.join(Rails.root, "public", "javascripts", "prototype.js"))
sources << "/javascripts/prototype.js"
else
sources << "http://ajax.googleapis.com/ajax/libs/prototype/1.6.1.0/prototype.js"
end
sources << "http://www.google.com/jsapi"
sources.map do |src|
%Q{<script src="#{src}" type="text/javascript"></script>}
end.join("\n")
end
end

View file

@ -0,0 +1,129 @@
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
describe TranslateController do
describe "index" do
before(:each) do
controller.stub!(:per_page).and_return(1)
I18n.backend.stub!(:translations).and_return(i18n_translations)
I18n.backend.instance_eval { @initialized = true }
keys = mock(:keys)
keys.stub!(:i18n_keys).and_return(['vendor.foobar'])
Translate::Keys.should_receive(:new).and_return(keys)
Translate::Keys.should_receive(:files).and_return(files)
I18n.stub!(:valid_locales).and_return([:en, :sv])
I18n.stub!(:default_locale).and_return(:sv)
end
it "shows sorted paginated keys from the translate from locale and extracted keys by default" do
get_page :index
assigns(:from_locale).should == :sv
assigns(:to_locale).should == :en
assigns(:files).should == files
assigns(:keys).sort.should == ['articles.new.page_title', 'home.page_title', 'vendor.foobar']
assigns(:paginated_keys).should == ['articles.new.page_title']
end
it "can be paginated with the page param" do
get_page :index, :page => 2
assigns(:files).should == files
assigns(:paginated_keys).should == ['home.page_title']
end
it "accepts a key_pattern param with key_type=starts_with" do
get_page :index, :key_pattern => 'articles', :key_type => 'starts_with'
assigns(:files).should == files
assigns(:paginated_keys).should == ['articles.new.page_title']
assigns(:total_entries).should == 1
end
it "accepts a key_pattern param with key_type=contains" do
get_page :index, :key_pattern => 'page_', :key_type => 'contains'
assigns(:files).should == files
assigns(:total_entries).should == 2
assigns(:paginated_keys).should == ['articles.new.page_title']
end
it "accepts a filter=untranslated param" do
get_page :index, :filter => 'untranslated'
assigns(:total_entries).should == 2
assigns(:paginated_keys).should == ['articles.new.page_title']
end
it "accepts a filter=translated param" do
get_page :index, :filter => 'translated'
assigns(:total_entries).should == 1
assigns(:paginated_keys).should == ['vendor.foobar']
end
it "accepts a filter=changed param" do
log = mock(:log)
old_translations = {:home => {:page_title => "Skapar ny artikel"}}
log.should_receive(:read).and_return(Translate::File.deep_stringify_keys(old_translations))
Translate::Log.should_receive(:new).with(:sv, :en, {}).and_return(log)
get_page :index, :filter => 'changed'
assigns(:total_entries).should == 1
assigns(:keys).should == ["home.page_title"]
end
def i18n_translations
HashWithIndifferentAccess.new({
:en => {
:vendor => {
:foobar => "Foo Baar"
}
},
:sv => {
:articles => {
:new => {
:page_title => "Skapa ny artikel"
}
},
:home => {
:page_title => "Välkommen till I18n"
},
:vendor => {
:foobar => "Fobar"
}
}
})
end
def files
HashWithIndifferentAccess.new({
:'home.page_title' => ["app/views/home/index.rhtml"],
:'general.back' => ["app/views/articles/new.rhtml", "app/views/categories/new.rhtml"],
:'articles.new.page_title' => ["app/views/articles/new.rhtml"]
})
end
end
describe "translate" do
it "should store translations to I18n backend and then write them to a YAML file" do
session[:from_locale] = :sv
session[:to_locale] = :en
translations = {
:articles => {
:new => {
:title => "New Article"
}
},
:category => "Category"
}
key_param = {'articles.new.title' => "New Article", "category" => "Category"}
I18n.backend.should_receive(:store_translations).with(:en, translations)
storage = mock(:storage)
storage.should_receive(:write_to_file)
Translate::Storage.should_receive(:new).with(:en).and_return(storage)
log = mock(:log)
log.should_receive(:write_to_file)
Translate::Log.should_receive(:new).with(:sv, :en, key_param.keys).and_return(log)
post :translate, "key" => key_param
response.should be_redirect
end
end
def get_page(*args)
get(*args)
response.should be_success
end
end

View file

@ -0,0 +1,54 @@
require 'fileutils'
require File.dirname(__FILE__) + '/spec_helper'
describe Translate::File do
describe "write" do
before(:each) do
@file = Translate::File.new(file_path)
end
after(:each) do
FileUtils.rm(file_path)
end
it "writes all I18n messages for a locale to YAML file" do
@file.write(translations)
@file.read.should == Translate::File.deep_stringify_keys(translations)
end
def translations
{
:en => {
:article => {
:title => "One Article"
},
:category => "Category"
}
}
end
end
describe "deep_stringify_keys" do
it "should convert all keys in a hash to strings" do
Translate::File.deep_stringify_keys({
:en => {
:article => {
:title => "One Article"
},
:category => "Category"
}
}).should == {
"en" => {
"article" => {
"title" => "One Article"
},
"category" => "Category"
}
}
end
end
def file_path
File.join(File.dirname(__FILE__), "files", "en.yml")
end
end

View file

@ -0,0 +1,12 @@
class Article < ActiveRecord::Base
def validate
# t('li')
errors.add_to_base([t(:'article.key1') + "#{t('article.key2')}"])
I18n.t 'article.key3'
I18n.t 'article.key3'
I18n.t :'article.key4'
I18n.translate :'article.key5'
'bla bla t' + "blubba bla" + ' foobar'
'bla bla t ' + "blubba bla" + ' foobar'
end
end

View file

@ -0,0 +1 @@
<%= t(:'category_erb.key1') %>

View file

@ -0,0 +1 @@
t(:'category_html.key1')

View file

@ -0,0 +1 @@
<%= t(:'category_html_erb.key1') %>

View file

@ -0,0 +1,5 @@
<script>
document.createElement('li');
</script>
<%= t(:'category_rhtml.key1') %>

View file

@ -0,0 +1 @@
I18n.t('js.alert')

View file

@ -0,0 +1,179 @@
require File.dirname(__FILE__) + '/spec_helper'
require 'fileutils'
describe Translate::Keys do
before(:each) do
I18n.stub!(:default_locale).and_return(:en)
@keys = Translate::Keys.new
Translate::Storage.stub!(:root_dir).and_return(i18n_files_dir)
end
describe "to_a" do
it "extracts keys from I18n lookups in .rb, .html.erb, and .rhtml files" do
@keys.to_a.map(&:to_s).sort.should == ['article.key1', 'article.key2', 'article.key3', 'article.key4', 'article.key5',
'category_erb.key1', 'category_html_erb.key1', 'category_rhtml.key1', 'js.alert']
end
end
describe "to_hash" do
it "return a hash with I18n keys and file lists" do
@keys.to_hash[:'article.key3'].should == ["vendor/plugins/translate/spec/files/translate/app/models/article.rb"]
end
end
describe "i18n_keys" do
before(:each) do
I18n.backend.send(:init_translations) unless I18n.backend.initialized?
end
it "should return all keys in the I18n backend translations hash" do
I18n.backend.should_receive(:translations).and_return(translations)
@keys.i18n_keys(:en).should == ['articles.new.page_title', 'categories.flash.created', 'empty', 'home.about']
end
describe "untranslated_keys" do
before(:each) do
I18n.backend.stub!(:translations).and_return(translations)
end
it "should return a hash with keys with missing translations in each locale" do
@keys.untranslated_keys.should == {
:sv => ['articles.new.page_title', 'categories.flash.created', 'empty']
}
end
end
describe "missing_keys" do
before(:each) do
@file_path = File.join(i18n_files_dir, "config", "locales", "en.yml")
Translate::File.new(@file_path).write({
:en => {
:home => {
:page_title => false,
:intro => {
:one => "intro one",
:other => "intro other"
}
}
}
})
end
after(:each) do
FileUtils.rm(@file_path)
end
it "should return a hash with keys that are not in the locale file" do
@keys.stub!(:files).and_return({
:'home.page_title' => "app/views/home/index.rhtml",
:'home.intro' => 'app/views/home/index.rhtml',
:'home.signup' => "app/views/home/_signup.rhtml",
:'about.index.page_title' => "app/views/about/index.rhtml"
})
@keys.missing_keys.should == {
:'home.signup' => "app/views/home/_signup.rhtml",
:'about.index.page_title' => "app/views/about/index.rhtml"
}
end
end
describe "contains_key?" do
it "works" do
hash = {
:foo => {
:bar => {
:baz => false
}
}
}
Translate::Keys.contains_key?(hash, "").should be_false
Translate::Keys.contains_key?(hash, "foo").should be_true
Translate::Keys.contains_key?(hash, "foo.bar").should be_true
Translate::Keys.contains_key?(hash, "foo.bar.baz").should be_true
Translate::Keys.contains_key?(hash, :"foo.bar.baz").should be_true
Translate::Keys.contains_key?(hash, "foo.bar.baz.bla").should be_false
end
end
describe "translated_locales" do
before(:each) do
I18n.stub!(:default_locale).and_return(:en)
I18n.stub!(:available_locales).and_return([:sv, :no, :en, :root])
end
it "returns all avaiable except :root and the default" do
Translate::Keys.translated_locales.should == [:sv, :no]
end
end
describe "to_deep_hash" do
it "convert shallow hash with dot separated keys to deep hash" do
Translate::Keys.to_deep_hash(shallow_hash).should == deep_hash
end
end
describe "to_shallow_hash" do
it "converts a deep hash to a shallow one" do
Translate::Keys.to_shallow_hash(deep_hash).should == shallow_hash
end
end
##########################################################################
#
# Helper Methods
#
##########################################################################
def translations
{
:en => {
:home => {
:about => "This site is about making money"
},
:articles => {
:new => {
:page_title => "New Article"
}
},
:categories => {
:flash => {
:created => "Category created"
}
},
:empty => nil
},
:sv => {
:home => {
:about => false
}
}
}
end
end
def shallow_hash
{
'pressrelease.label.one' => "Pressmeddelande",
'pressrelease.label.other' => "Pressmeddelanden",
'article' => "Artikel",
'category' => ''
}
end
def deep_hash
{
:pressrelease => {
:label => {
:one => "Pressmeddelande",
:other => "Pressmeddelanden"
}
},
:article => "Artikel",
:category => ''
}
end
def i18n_files_dir
File.join(ENV['PWD'], "spec", "files", "translate")
end
end

View file

@ -0,0 +1,47 @@
require 'fileutils'
require File.dirname(__FILE__) + '/spec_helper'
describe Translate::Log do
describe "write_to_file" do
before(:each) do
I18n.locale = :sv
I18n.backend.store_translations(:sv, from_texts)
keys = Translate::Keys.new
@log = Translate::Log.new(:sv, :en, Translate::Keys.to_shallow_hash(from_texts).keys)
@log.stub!(:file_path).and_return(file_path)
FileUtils.rm_f file_path
end
after(:each) do
FileUtils.rm_f file_path
end
it "writes new log file with from texts" do
File.exists?(file_path).should be_false
@log.write_to_file
File.exists?(file_path).should be_true
Translate::File.new(file_path).read.should == Translate::File.deep_stringify_keys(from_texts)
end
it "merges from texts with current texts in log file and re-writes the log file" do
@log.write_to_file
I18n.backend.store_translations(:sv, {:category => "Kategori ny"})
@log.keys = ['category']
@log.write_to_file
Translate::File.new(file_path).read['category'].should == "Kategori ny"
end
def file_path
File.join(File.dirname(__FILE__), "files", "from_sv_to_en.yml")
end
def from_texts
{
:article => {
:title => "En artikel"
},
:category => "Kategori"
}
end
end
end

View file

@ -0,0 +1,11 @@
begin
# Using PWD here instead of File.dirname(__FILE__) to be able to symlink to plugin
# from within a Rails app.
require File.expand_path(ENV['PWD'] + '/../../../spec/spec_helper')
rescue LoadError => e
puts "You need to install rspec in your base app\n#{e.message}: #{e.backtrace.join("\n")}"
exit
end
plugin_spec_dir = File.dirname(__FILE__)
ActiveRecord::Base.logger = Logger.new(plugin_spec_dir + "/debug.log")

View file

@ -0,0 +1,33 @@
require File.dirname(__FILE__) + '/spec_helper'
describe Translate::Storage do
describe "write_to_file" do
before(:each) do
@storage = Translate::Storage.new(:en)
end
it "writes all I18n messages for a locale to YAML file" do
I18n.backend.should_receive(:translations).and_return(translations)
@storage.stub!(:file_path).and_return(file_path)
file = mock(:file)
file.should_receive(:write).with(translations)
Translate::File.should_receive(:new).with(file_path).and_return(file)
@storage.write_to_file
end
def file_path
File.join(File.dirname(__FILE__), "files", "en.yml")
end
def translations
{
:en => {
:article => {
:title => "One Article"
},
:category => "Category"
}
}
end
end
end

View file

@ -0,0 +1,178 @@
require 'yaml'
class Hash
def deep_merge(other)
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
merger = proc { |key, v1, v2| (Hash === v1 && Hash === v2) ? v1.merge(v2, &merger) : v2 }
merge(other, &merger)
end
def set(keys, value)
key = keys.shift
if keys.empty?
self[key] = value
else
self[key] ||= {}
self[key].set keys, value
end
end
if ENV['SORT']
# copy of ruby's to_yaml method, prepending sort.
# before each so we get an ordered yaml file
def to_yaml( opts = {} )
YAML::quick_emit( self, opts ) do |out|
out.map( taguri, to_yaml_style ) do |map|
sort.each do |k, v| #<- Adding sort.
map.add( k, v )
end
end
end
end
end
end
namespace :translate do
desc "Show untranslated keys for locale LOCALE"
task :untranslated => :environment do
from_locale = I18n.default_locale
untranslated = Translate::Keys.new.untranslated_keys
messages = []
untranslated.each do |locale, keys|
keys.each do |key|
from_text = I18n.backend.send(:lookup, from_locale, key)
messages << "#{locale}.#{key} (#{from_locale}.#{key}='#{from_text}')"
end
end
if messages.present?
messages.each { |m| puts m }
else
puts "No untranslated keys"
end
end
desc "Show I18n keys that are missing in the config/locales/default_locale.yml YAML file"
task :missing => :environment do
missing = Translate::Keys.new.missing_keys.inject([]) do |keys, (key, filename)|
keys << "#{key} in \t #{filename} is missing"
end
puts missing.present? ? missing.join("\n") : "No missing translations in the default locale file"
end
desc "Remove all translation texts that are no longer present in the locale they were translated from"
task :remove_obsolete_keys => :environment do
I18n.backend.send(:init_translations)
master_locale = ENV['LOCALE'] || I18n.default_locale
Translate::Keys.translated_locales.each do |locale|
texts = {}
Translate::Keys.new.i18n_keys(locale).each do |key|
if I18n.backend.send(:lookup, master_locale, key).to_s.present?
texts[key] = I18n.backend.send(:lookup, locale, key)
end
end
I18n.backend.send(:translations)[locale] = nil # Clear out all current translations
I18n.backend.store_translations(locale, Translate::Keys.to_deep_hash(texts))
Translate::Storage.new(locale).write_to_file
end
end
desc "Merge I18n keys from log/translations.yml into config/locales/*.yml (for use with the Rails I18n TextMate bundle)"
task :merge_keys => :environment do
I18n.backend.send(:init_translations)
new_translations = YAML::load(IO.read(File.join(Rails.root, "log", "translations.yml")))
raise("Can only merge in translations in single locale") if new_translations.keys.size > 1
locale = new_translations.keys.first
overwrites = false
Translate::Keys.to_shallow_hash(new_translations[locale]).keys.each do |key|
new_text = key.split(".").inject(new_translations[locale]) { |hash, sub_key| hash[sub_key] }
existing_text = I18n.backend.send(:lookup, locale.to_sym, key)
if existing_text && new_text != existing_text
puts "ERROR: key #{key} already exists with text '#{existing_text.inspect}' and would be overwritten by new text '#{new_text}'. " +
"Set environment variable OVERWRITE=1 if you really want to do this."
overwrites = true
end
end
if !overwrites || ENV['OVERWRITE']
I18n.backend.store_translations(locale, new_translations[locale])
Translate::Storage.new(locale).write_to_file
end
end
desc "Apply Google translate to auto translate all texts in locale ENV['FROM'] to locale ENV['TO']"
task :google => :environment do
raise "Please specify FROM and TO locales as environment variables" if ENV['FROM'].blank? || ENV['TO'].blank?
# Depends on httparty gem
# http://www.robbyonrails.com/articles/2009/03/16/httparty-goes-foreign
class GoogleApi
include HTTParty
base_uri 'ajax.googleapis.com'
def self.translate(string, to, from)
tries = 0
begin
get("/ajax/services/language/translate",
:query => {:langpair => "#{from}|#{to}", :q => string, :v => 1.0},
:format => :json)
rescue
tries += 1
puts("SLEEPING - retrying in 5...")
sleep(5)
retry if tries < 10
end
end
end
I18n.backend.send(:init_translations)
start_at = Time.now
translations = {}
Translate::Keys.new.i18n_keys(ENV['FROM']).each do |key|
from_text = I18n.backend.send(:lookup, ENV['FROM'], key).to_s
to_text = I18n.backend.send(:lookup, ENV['TO'], key)
if !from_text.blank? && to_text.blank?
print "#{key}: '#{from_text[0, 40]}' => "
if !translations[from_text]
response = GoogleApi.translate(from_text, ENV['TO'], ENV['FROM'])
translations[from_text] = response["responseData"] && response["responseData"]["translatedText"]
end
if !(translation = translations[from_text]).blank?
translation.gsub!(/\(\(([a-z_.]+)\)\)/i, '{{\1}}')
# Google translate sometimes replaces {{foobar}} with (()) foobar. We skip these
if translation !~ /\(\(\)\)/
puts "'#{translation[0, 40]}'"
I18n.backend.store_translations(ENV['TO'].to_sym, Translate::Keys.to_deep_hash({key => translation}))
else
puts "SKIPPING since interpolations were messed up: '#{translation[0,40]}'"
end
else
puts "NO TRANSLATION - #{response.inspect}"
end
end
end
puts "\nTime elapsed: #{(((Time.now - start_at) / 60) * 10).to_i / 10.to_f} minutes"
Translate::Storage.new(ENV['TO'].to_sym).write_to_file
end
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"
task :changed => :environment do
from_hash = Translate::Keys.to_shallow_hash(Translate::File.new(ENV['FROM_FILE']).read)
to_hash = Translate::Keys.to_shallow_hash(Translate::File.new(ENV['TO_FILE']).read)
from_hash.each do |key, from_value|
if (to_value = to_hash[key]) && to_value != from_value
key_without_locale = key[/^[^.]+\.(.+)$/, 1]
if ENV['VERBOSE']
puts "KEY: #{key_without_locale}"
puts "FROM VALUE: '#{from_value}'"
puts "TO VALUE: '#{to_value}'"
else
puts key_without_locale
end
end
end
end
end

View file

@ -0,0 +1,359 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title><%= h(@page_title) %></title>
<%= translate_javascript_includes %>
<script type="text/javascript">
google.load("language", "1");
function getGoogleTranslation(id, text, from_language, to_language) {
text = text.replace(/\{\{/, '__').replace(/\}\}/, '__')
google.language.translate(text, from_language, to_language, function(result) {
if (!result.error) {
result_text = result.translation.unescapeHTML().gsub(/__(.+)__/, function(match){
return '{{' + match[1] + '}}';
});
Form.Element.setValue(id, result_text);
}
});
}
/*
prototypeUtils.js from http://jehiah.com/
Licensed under Creative Commons.
version 1.0 December 20 2005
Contains:
+ Form.Element.setValue()
+ unpackToForm()
*/
/* Form.Element.setValue("fieldname/id","valueToSet") */
Form.Element.setValue = function(element,newValue) {
element_id = element;
element = $(element);
if (!element){element = document.getElementsByName(element_id)[0];}
if (!element){return false;}
var method = element.tagName.toLowerCase();
var parameter = Form.Element.SetSerializers[method](element,newValue);
}
Form.Element.SetSerializers = {
input: function(element,newValue) {
switch (element.type.toLowerCase()) {
case 'submit':
case 'hidden':
case 'password':
case 'text':
return Form.Element.SetSerializers.textarea(element,newValue);
case 'checkbox':
case 'radio':
return Form.Element.SetSerializers.inputSelector(element,newValue);
}
return false;
},
inputSelector: function(element,newValue) {
fields = document.getElementsByName(element.name);
for (var i=0;i<fields.length;i++){
if (fields[i].value == newValue){
fields[i].checked = true;
}
}
},
textarea: function(element,newValue) {
element.value = newValue;
},
select: function(element,newValue) {
var value = '', opt, index = element.selectedIndex;
for (var i=0;i< element.options.length;i++){
if (element.options[i].value == newValue){
element.selectedIndex = i;
return true;
}
}
}
}
function unpackToForm(data){
for (i in data){
Form.Element.setValue(i,data[i].toString());
}
}
</script>
<style type="text/css">
/*reset.css*/
/* v1.0 | 20080212 */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
}
body {
line-height: 1;
}
ol, ul {
list-style: none;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
/* remember to define focus styles! */
:focus {
outline: 0;
}
/* remember to highlight inserts somehow! */
ins {
text-decoration: none;
}
del {
text-decoration: line-through;
}
/* tables still need 'cellspacing="0"' in the markup */
table {
border-collapse: collapse;
border-spacing: 0;
}
/*clear fix*/
.clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden;}
.clearfix{display:inline-block;}
html[xmlns] .clearfix {
display: block;
}
* html .clearfix{height:1%;}
/*start layout*/
body{
background:#fff;
color:#333;
font-size:75%;
font-family:Arial;
margin:2em auto;
line-height:1.5em;
}
textarea,input,select{
font-family:Arial;
font-size:1em;
}
h1{
color:#d46021;
font-size:2em;
margin-bottom:0.5em;
}
h2{
text-align:left;
color:#d46021;
font-size:1.3em;
padding-left:0;
}
a{
color:#2158C7;
}
div#container{
width:960px;
margin:0 auto;
font-size:1em;
}
/*paging*/
div.paging{
margin-bottom:1em;
text-align:left;
}
div.paging div{
border:solid 1px red;
margin:1em 1em 0;
padding:0.5em;
border:solid 1px #d5d6d5;
background:#f1f1f1;
}
ul.paging{
display:inline-block;
}
ul.paging li{
display:block;
margin:0.2em 0;
float:left;
}
ul.paging li.selected a{
color:#fff;
background:#2158C7;
font-weight:bold;
padding:0.5em 0.7em;
}
ul.paging li a{
display:inline-block;
line-height:1em;
padding:0.5em 0.5em;
}
/*forms filter*/
fieldset{
padding:1em;
margin:1em;
border:solid 2px #d46021;
}
legend{
font-size:1.2em;
font-weight:bold;
padding:0 1em;
padding-bottom:0.5em;
}
label{
font-weight:bold;
}
fieldset span{padding-right:0.5em;}
div#show-sort label,
div#languages label,
div#filter-pattern label{
display:inline-block;
width:100px;
line-height:2em;
}
div#show-sort select,
div#languages select,
div#filter-pattern select{
width:120px;
margin-right:0.5em;
}
div#show-sort input.text-default,
div#languages input.text-default,
div#filter-pattern input.text-default{
width:200px;
}
p.hits{
margin-top:1em;
}
/*translation edit*/
div.translations{
margin:1em;
padding:1em;
border:solid 2px #d46021;
}
div.translations h2{
margin-bottom:1em;
}
p.translate{
background:red;
border:solid 1px #d5d6d5;
background:#f1f1f1;
margin:0.5em;
padding:0.7em 0.5em 0.5em 1.5em;
}
div.translation{
padding:1em;
border-bottom:solid 0.2em #d46021;
margin:0 1em 1em 1.6em;
}
div.translation input, div.translation textarea{
width:98%;
margin:1em 0;
display:inline-block;
padding:0.3em;
}
div.translation textarea{
height:50px;
}
div.translation em strong{
color:#333;
padding-right:0.5em;
}
p.translation em{
display:block;
font-size:0.8333em;
}
div.translation a{
padding:1em;
}
div.translation input.btnDefault{
margin:0 0 1em;
width:auto;
}
.focus-text{
font-weight:bold;
}
div.selected{
margin:0 1em 1em 1em;
border-left:solid 0.6em #d46021;
border-right:solid 0.2em #d46021;
border-top:solid 0.2em #d46021;
background:#f1f1f1;
}
.display{display:block !important;}
/*feedback*/
div#notice, div#error {
font-size:1em;
margin:1em;
padding: 1em;
border: 1px solid red;
}
div#notice span, div#error span{
font-size:1.5em;
}
div#error {
background-color: #F3C6CC;
color: red;
}
div#notice {
border-color: #72A974;
color: #597B5C;
background-color: #BCFFBD;
}
</style>
<script type="text/javascript">
onload = function (){
$$("div.translation input, div.translation textarea").each(function (e){
Event.observe(e,'focus', function (elm){
this.up(1).down(".translation-text").addClassName("focus-text");
this.up(1).addClassName("selected");
});
Event.observe(e,'blur', function (elm,e){
this.up(1).down(".translation-text").removeClassName("focus-text");
this.up(1).removeClassName("selected");
});
});
}
</script>
</head>
<body>
<div id="container">
<% if @page_title -%><h1><%=h @page_title %></h1><% end -%>
<% [:notice, :error].each do |message| %>
<%if flash[message] %>
<div id="<%= message %>">
<span><%= h(flash[message]) if flash[message] %></span>
</div>
<% end %>
<% end %>
<%= yield %>
</div>
</body>
</html>

View file

@ -0,0 +1,24 @@
<%
# Expects locals:
#
# total_entries
# per_page
n_pages = total_entries/per_page + (total_entries % per_page > 0 ? 1 : 0)
current_page = (params[:page] || 1).to_i
%>
<% if n_pages > 1 %>
<h2>Pages:</h2>
<div class="clearfix">
<ul class="paging">
<% (1..n_pages).each do |page_number| %>
<% if current_page == page_number %>
<li class="selected"><%= link_to(page_number, params.merge(:page => page_number), :title => "Page #{page_number}" ) %></li>
<% else %>
<li><%= link_to(page_number, params.merge(:page => page_number), :title => "Page #{page_number}") %></li>
<% end %>
<% end %>
</ul>
</div>
<% end %>

View file

@ -0,0 +1,114 @@
<%
@page_title = "Translate"
show_filters = ["all", "untranslated", "translated"]
show_filters << "changed" if @from_locale != @to_locale
%>
<fieldset>
<legend>Search filter</legend>
<div id="show-sort">
<p>
<label>Show:</label> <%= simple_filter(show_filters) %>
</p>
<p>
<label>Sort by:</label> <%= simple_filter(["key", "text"], 'sort_by') %>
</p>
</div>
<% form_tag(params, :method => :get) do %>
<div id="languages">
<p>
<%= hidden_field_tag(:filter, params[:filter]) %>
<%= hidden_field_tag(:sort_by, params[:sort_by]) %>
<label>Translate from</label>
<%= select_tag(:from_locale, options_for_select(I18n.valid_locales, @from_locale.to_sym)) %> <span>to</span>
<%= select_tag(:to_locale, options_for_select(I18n.valid_locales, @to_locale.to_sym)) %>
<%= submit_tag "Display" %>
</p>
</div>
<div id="filter-pattern">
<p>
<label for="key_pattern_value">Key</label>
<%= select_tag(:key_type, options_for_select([["contains", 'contains'], ["starts with", 'starts_with']], params[:key_type])) %>
<%= text_field_tag(:key_pattern, params[:key_pattern], :size => 50, :id => "key_pattern_value", :class => "text-default") %>
</p>
<p>
<label for="text_pattern_value">Text</label>
<%= select_tag(:text_type, options_for_select(['contains', 'equals'], params[:text_type])) %>
<%= text_field_tag(:text_pattern, params[:text_pattern], :size => 50, :id => "text_pattern_value", :class => "text-default") %>
</p>
<p>
<%= submit_tag "Search" %>
<%= link_to "clear", params.merge({:text_pattern => nil, :key_pattern => nil}) %>
</p>
</div>
<% end %>
<p class="hits">
Found <strong><%= @total_entries %></strong> messages
</p>
<p>
<%= link_to "Reload messages", translate_reload_path %>
</p>
</fieldset>
<div class="paging">
<%= render :partial => 'pagination', :locals => {:total_entries => @total_entries, :per_page => per_page} %>
</div>
<% if @total_entries > 0 %>
<% form_tag(translate_path) do %>
<div>
<%= hidden_field_tag(:filter, params[:filter], :id => "hid_filter") %>
<%= hidden_field_tag(:sort_by, params[:sort_by], :id => "hid_sort_by") %>
<%= hidden_field_tag(:key_type, params[:key_type], :id => "hid_key_type") %>
<%= hidden_field_tag(:key_pattern, params[:key_pattern], :id => "hid_key_pattern") %>
<%= hidden_field_tag(:text_type, params[:text_type], :id => "hid_text_type") %>
<%= hidden_field_tag(:text_pattern, params[:text_pattern], :id => "hid_text_pattern") %>
</div>
<div class="translations">
<h2>Translations from <%= @from_locale %> to <%= @to_locale %></h2>
<p class="translate">
<%= submit_tag "Save Translations" %>
</p>
<% @paginated_keys.each do |key|
from_text = lookup(@from_locale, key)
to_text = lookup(@to_locale, key)
line_size = 100
n_lines = n_lines(from_text, line_size)
field_name = "key[#{key}]"
%>
<div class="translation">
<% if from_text.present? %>
<p class="translation-text">
<%= simple_format(h(from_text)) %>
</p>
<% end %>
<p class="edit-form">
<% if n_lines > 1 %>
<%= text_area_tag(field_name, to_text, :size => "#{line_size}x#{n_lines}", :id => key) %>
<% else %>
<%= text_field_tag(field_name, to_text, :size => line_size, :id => key) %>
<% end %>
</p>
<p>
<em>
<%= link_to_function 'Auto Translate', "getGoogleTranslation('#{key}', \"#{escape_javascript(from_text)}\", '#{@from_locale}', '#{@to_locale}')", :style => 'padding: 0; margin: 0;' %>
<br/>
<strong>Key:</strong><%=h key %><br/>
<% if @files[key] %>
<strong>File:</strong><%= @files[key].join("<br/>") %>
<% end %>
</em>
</p>
</div>
<% end %>
<p class="translate">
<%= submit_tag "Save Translations" %>
</p>
</div>
<% end %>
<% end %>
<div class="paging">
<%= render :partial => 'pagination', :locals => {:total_entries => @total_entries, :per_page => per_page} %>
</div>