i18n_googledocs 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,157 @@
1
+ require 'pathname'
2
+ require 'fileutils'
3
+
4
+ module I18nDocs
5
+ module Generators
6
+ class NormalizeGenerator < Rails::Generators::Base
7
+ desc "Normalize locale files by converting tabs to spaces"
8
+
9
+ class_option :spaces, :type => :string, :default => '2',
10
+ :desc => "Spaces for each tab"
11
+
12
+ class_option :overwrite, :type => :boolean, :default => false,
13
+ :desc => "Overwrite existing locale files?"
14
+
15
+ class_option :delete, :type => :boolean, :default => false,
16
+ :desc => "Delete old normalized files?"
17
+
18
+ class_option :accept, :type => :boolean, :default => false,
19
+ :desc => "Accept normalized files"
20
+
21
+ class_option :debug, :type => :boolean, :default => false,
22
+ :desc => "Turn debug mode on?"
23
+
24
+ argument :locales, :type => :array, :default => [],
25
+ :desc => "List of locales to normalize"
26
+
27
+ def main
28
+ delete_normalized and return if delete?
29
+ accept_normalized and return if accept?
30
+ tabs_to_spaces
31
+ end
32
+
33
+ protected
34
+
35
+ def delete?
36
+ options[:delete]
37
+ end
38
+
39
+ def debug?
40
+ options[:delete]
41
+ end
42
+
43
+ def accept?
44
+ options[:accept]
45
+ end
46
+
47
+ def delete_normalized
48
+ locales.each {|locale| delete_for locale }
49
+ end
50
+
51
+ def delete_for locale = :all
52
+ path = locale_path(locale)
53
+ say "Deleting normalized files for: #{locale}"
54
+ normalized_files(path).each do |file|
55
+ say "Deleting: #{file}" if debug?
56
+ File.delete file
57
+ end
58
+ end
59
+
60
+ def accept_normalized
61
+ locales.each {|locale| accept_for locale }
62
+ end
63
+
64
+ def accept_for locale = :all
65
+ path = locale_path(locale)
66
+ say "Accepting normalized files for: #{locale}"
67
+ normalized_files(path).each do |file|
68
+ new_file_name = File.basename(file).gsub /^_/, ''
69
+ file_path = File.join(File.dirname(file), new_file_name)
70
+ FileUtils.mv file, file_path
71
+
72
+ say "Accepted for: #{new_file_name}" if debug?
73
+ end
74
+ end
75
+
76
+
77
+ def tabs_to_spaces
78
+ say "Normalizing tabs for locales: #{locales} - with #{spaces} spaces"
79
+ locales.empty? ? normalize_for(:all) : for_locales
80
+ end
81
+
82
+ def for_locales
83
+ locales.each {|locale| normalize_for locale }
84
+ end
85
+
86
+ def normalize_for locale = :en
87
+ path = locale_path(locale)
88
+ replacement = spaces_pr_tab
89
+ say "Normalizing for: #{locale}" if debug?
90
+ say "In folder: #{path}" if debug?
91
+
92
+ files(path).each do |file|
93
+ normalize_file_content file
94
+ end
95
+ say "Normalize completed", :green
96
+ end
97
+
98
+ def locale_path locale
99
+ (locale != :all) ? File.join(locales_root, locale) : locales_root
100
+ end
101
+
102
+ def normalize_file_content file
103
+ say "Normalizing file: #{file}" if debug?
104
+
105
+ content = File.open(file).read
106
+ replaced_content = content.gsub /\t/, spaces_pr_tab
107
+ replaced_content = content.gsub /^---/, ''
108
+ replaced_content = content.gsub /^no:/, "'no':"
109
+
110
+ File.open(new_file(file), 'w') do |f|
111
+ f.puts replaced_content
112
+ end
113
+ end
114
+
115
+ def new_file file
116
+ overwrite? ? file : normalized_file_name(file)
117
+ end
118
+
119
+ def normalized_file_name file_name
120
+ new_file_name = '_' + File.basename(file_name)
121
+ File.join(File.dirname(file_name), new_file_name)
122
+ end
123
+
124
+ def overwrite?
125
+ options[:overwrite]
126
+ end
127
+
128
+ def files path
129
+ Dir[File.join(path,'*.yml')]
130
+ end
131
+
132
+ def normalized_files path
133
+ Dir[File.join(path,'_*.yml')]
134
+ end
135
+
136
+ def unnormalized_files path
137
+ Dir[File.join(path,'[^_]*.yml')]
138
+ end
139
+
140
+ def spaces_pr_tab
141
+ @spaces ||= (1..spaces).to_a.inject("") {|res, e| res << ' ' }
142
+ end
143
+
144
+ def spaces
145
+ options[:spaces].to_i
146
+ end
147
+
148
+ def num_spaces
149
+ spaces.to_i
150
+ end
151
+
152
+ def locales_root
153
+ Rails.root.join 'config', 'locales'
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,2 @@
1
+ <%= locale %>:
2
+ <%= key %>: <%= text %>
@@ -0,0 +1,51 @@
1
+ # require 'rake'
2
+
3
+ # load rake tasks in case GEM is included within rails project
4
+
5
+ require 'faster_csv'
6
+ require 'yaml'
7
+ require 'open-uri'
8
+ require 'localch_i18n/util'
9
+ require 'localch_i18n/missing_keys_finder'
10
+ require 'localch_i18n/csv_to_yaml'
11
+ require 'localch_i18n/translations'
12
+ require 'localch_i18n/translation_file_export'
13
+ require 'localch_i18n/translator'
14
+
15
+ module I18nDocs
16
+ class << self
17
+ attr_accessor :debug
18
+
19
+ def debug?
20
+ @debug
21
+ end
22
+
23
+ def debug!
24
+ @debug = true
25
+ end
26
+
27
+ def add_locale_paths_for locales
28
+ locales.each do |locale|
29
+ path = Rails.root.join('config', 'locales', locale.to_s, '*.yml')
30
+ puts "Adding locale path: #{path}" if debug?
31
+ I18n.load_path += Dir[path]
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+
38
+ module I18n
39
+ class << self
40
+ attr_accessor :google_translation_key
41
+ end
42
+ end
43
+
44
+ if defined?(Rails)
45
+ class LocalchI18nTask < Rails::Railtie
46
+ rake_tasks do
47
+ Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f }
48
+ end
49
+ end
50
+ end
51
+
@@ -0,0 +1,68 @@
1
+ module LocalchI18n
2
+
3
+ class CsvToYaml
4
+
5
+ attr_reader :input_file, :output_file, :locales, :translations
6
+
7
+ def initialize(input_file, output_file, locales = [])
8
+ @input_file = input_file
9
+ @output_file = File.basename(output_file)
10
+ @locales = locales.map(&:to_s)
11
+
12
+ # init translation hash
13
+ @translations = {}
14
+ @locales.each do |locale|
15
+ @translations[locale] = {}
16
+ end
17
+ end
18
+
19
+
20
+ def write_files
21
+ @locales.each do |locale|
22
+ output_file_path = defined?(Rails) ? Rails.root.join('config', 'locales', locale, @output_file) : "#{locale}_#{@output_file}"
23
+ File.open(output_file_path, 'w') do |file|
24
+ final_translation_hash = {locale => @translations[locale]}
25
+ file.puts YAML::dump(final_translation_hash)
26
+ end
27
+ puts "File '#{@output_file}' for language '#{locale}' written to disc (#{output_file_path})"
28
+ end
29
+ end
30
+
31
+
32
+ def process
33
+ FasterCSV.foreach(@input_file, :headers => true) do |row|
34
+ process_row(row.to_hash)
35
+ end
36
+ end
37
+
38
+ def process_row(row_hash)
39
+ key = row_hash.delete('key')
40
+
41
+ key_elements = key.split('.')
42
+ @locales.each do |locale|
43
+ raise "Locale missing for key #{key}! (locales in app: #{@locales} / locales in file: #{row_hash.keys.to_s})" if !row_hash.has_key?(locale)
44
+ store_translation(key_elements, locale, row_hash[locale])
45
+ end
46
+ end
47
+
48
+
49
+ def store_translation(keys, locale, value)
50
+ return nil if value.nil? # we don't store keys that don't have a valid value
51
+ # Google Spreadsheet does not export empty strings and therefore we use '_' as a replacement char.
52
+ value = '' if value == '_'
53
+
54
+ tree = keys[0...-1]
55
+ leaf = keys.last
56
+ data_hash = tree.inject(@translations[locale]) do |memo, k|
57
+ if memo.has_key?(k)
58
+ memo[k]
59
+ else
60
+ memo[k] = {}
61
+ end
62
+ end
63
+ data_hash[leaf] = value
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,111 @@
1
+ module LocalchI18n
2
+ class MissingKeysFinder
3
+ def initialize(backend)
4
+ @backend = backend
5
+ self.load_config
6
+ self.load_translations
7
+ end
8
+
9
+ # Returns an array with all keys from all locales
10
+ def all_keys
11
+ I18n.backend.send(:translations).collect do |check_locale, translations|
12
+ collect_keys([], translations).sort
13
+ end.flatten.uniq
14
+ end
15
+
16
+ def find_missing_keys
17
+ output_available_locales
18
+ output_unique_key_stats(all_keys)
19
+
20
+ missing_keys = {}
21
+ all_keys.each do |key|
22
+
23
+ I18n.available_locales.each do |locale|
24
+
25
+ skip = false
26
+ ls = locale.to_s
27
+ if !@yaml[ls].nil?
28
+ @yaml[ls].each do |re|
29
+ if key.match(re)
30
+ skip = true
31
+ break
32
+ end
33
+ end
34
+ end
35
+
36
+ if !key_exists?(key, locale) && skip == false
37
+ if missing_keys[key]
38
+ missing_keys[key] << locale
39
+ else
40
+ missing_keys[key] = [locale]
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ output_missing_keys(missing_keys)
47
+ return missing_keys
48
+ end
49
+
50
+ def output_available_locales
51
+ puts "#{I18n.available_locales.size} #{I18n.available_locales.size == 1 ? 'locale' : 'locales'} available: #{I18n.available_locales.join(', ')}"
52
+ end
53
+
54
+ def output_missing_keys(missing_keys)
55
+ if missing_keys.size > 0
56
+ puts "#{missing_keys.size} #{missing_keys.size == 1 ? 'key is missing' : 'keys are missing'} from one or more locales:"
57
+ missing_keys.keys.sort.each do |key|
58
+ puts "'#{key}': Missing from #{missing_keys[key].collect(&:inspect).join(', ')}"
59
+ end
60
+ puts "\nERROR: #{missing_keys.size} #{missing_keys.size == 1 ? 'key is missing' : 'keys are missing'} from one or more locales."
61
+ else
62
+ puts "No keys are missing"
63
+ end
64
+ end
65
+
66
+ def output_unique_key_stats(keys)
67
+ number_of_keys = keys.size
68
+ puts "#{number_of_keys} #{number_of_keys == 1 ? 'unique key' : 'unique keys'} found."
69
+ end
70
+
71
+ def collect_keys(scope, translations)
72
+ full_keys = []
73
+ translations.to_a.each do |key, translations|
74
+ new_scope = scope.dup << key
75
+ if translations.is_a?(Hash)
76
+ full_keys += collect_keys(new_scope, translations)
77
+ else
78
+ full_keys << new_scope.join('.')
79
+ end
80
+ end
81
+ return full_keys
82
+ end
83
+
84
+ # Returns true if key exists in the given locale
85
+ def key_exists?(key, locale)
86
+ I18n.locale = locale
87
+ I18n.translate(key, :raise => true)
88
+ return true
89
+ rescue I18n::MissingInterpolationArgument
90
+ return true
91
+ rescue I18n::MissingTranslationData
92
+ return false
93
+ end
94
+
95
+ def load_translations
96
+ # Make sure we’ve loaded the translations
97
+ I18n.backend.send(:init_translations)
98
+ end
99
+
100
+ def load_config
101
+ @yaml = {}
102
+ begin
103
+ @yaml = YAML.load_file(File.join(Rails.root, 'config', 'ignore_missing_i18n_keys.yml'))
104
+ rescue => e
105
+ STDERR.puts "No ignore_missing_keys.yml config file."
106
+ end
107
+
108
+ end
109
+
110
+ end
111
+ end
@@ -0,0 +1,76 @@
1
+ module LocalchI18n
2
+ class TranslationFileExport
3
+ include LocalchI18n::Util
4
+
5
+ attr_accessor :translations
6
+ attr_reader :main_locale, :current_locale
7
+
8
+ def initialize(source_dir, source_file, output_dir, locales, options = {})
9
+ @source_dir = source_dir
10
+ @source_file = source_file
11
+ @auto_translate = options[:auto_translate]
12
+
13
+ @output_file = File.join(output_dir, source_file.gsub('.yml', '.csv'))
14
+ @locales = locales.map {|l| l.to_s.downcase }
15
+
16
+ @translations = {}
17
+ end
18
+
19
+
20
+ def export
21
+ load_translations
22
+ write_to_csv
23
+ end
24
+
25
+ def write_to_csv
26
+ @main_locale = main_locale = @locales.include?('en') ? 'en' : @locales.first
27
+
28
+ puts " #{@source_file}: write CSV to '#{@output_file}' \n\n"
29
+
30
+ FasterCSV.open(@output_file, "wb") do |csv|
31
+ csv << (["key"] + @locales)
32
+
33
+ if @translations.empty? || !@translations[main_locale] || @translations[main_locale].keys.empty?
34
+ puts %Q{Translations #{@source_file} for #{main_locale} could not be processed, likely due to a YAML syntax error.
35
+ Please try again with the --normalize option.
36
+
37
+ The problem could also be due to an invalid locale code. Please check the i18n.available_locales setting in config/application.rb}
38
+ exit(0)
39
+ end
40
+
41
+ @translations[main_locale].keys.each do |key|
42
+ values = @locales.map do |locale|
43
+ @translations[locale][key] if @translations[locale]
44
+ end
45
+ csv << values.unshift(key)
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+
52
+ def load_translations
53
+ @locales.each do |locale|
54
+ @current_locale = locale
55
+ translation_hash = load_language(locale)
56
+ unless translation_hash.blank?
57
+ @translations[locale] = flatten_translations_hash(translation_hash)
58
+ else
59
+ puts "Error: No translations for locale - #{locale}"
60
+ end
61
+ end
62
+ end
63
+
64
+ def load_language(locale)
65
+
66
+ puts " #{@source_file}: load translations for '#{locale}'"
67
+
68
+ input_file = File.join(@source_dir, locale, @source_file)
69
+
70
+ # puts " input file: #{input_file}"
71
+ load_translations_for input_file, locale
72
+ end
73
+
74
+ end
75
+
76
+ end
@@ -0,0 +1,70 @@
1
+ #
2
+ # Order of method calls
3
+ # download_files
4
+ # store_translations
5
+ # clean_up
6
+ #
7
+ module LocalchI18n
8
+ class Translations
9
+
10
+ attr_accessor :locales, :tmp_folder, :config_file, :csv_files
11
+
12
+ def initialize(config_file = nil, tmp_folder = nil)
13
+ @config_file = config_file
14
+ @tmp_folder = tmp_folder
15
+
16
+ @csv_files = {}
17
+
18
+ load_config
19
+ load_locales
20
+ end
21
+
22
+ def load_locales
23
+ @locales = []
24
+ @locales = I18n.available_locales if defined?(I18n)
25
+ end
26
+
27
+ def load_config
28
+ @settings = {}
29
+ @settings = YAML.load_file(config_file) if File.exists?(config_file)
30
+ end
31
+
32
+ def download_files
33
+ files = @settings['files']
34
+ files.each do |target_file, url|
35
+ # download file to tmp directory
36
+ tmp_file = File.basename(target_file).gsub('.yml', '.csv')
37
+ tmp_file = File.join(@tmp_folder, tmp_file)
38
+ download(url, tmp_file)
39
+ @csv_files[target_file] = tmp_file
40
+ end
41
+ end
42
+
43
+ def store_translations
44
+ @csv_files.each do |target_file, csv_file|
45
+ converter = CsvToYaml.new(csv_file, target_file, @locales)
46
+ converter.process
47
+ converter.write_files
48
+ end
49
+
50
+ @csv_files
51
+ end
52
+
53
+ def clean_up
54
+ # remove all tmp files
55
+ @csv_files.each do |target_file, csv_file|
56
+ File.unlink(csv_file)
57
+ end
58
+ end
59
+
60
+ def download(url, destination_file)
61
+ puts "Download '#{url}' to '#{destination_file}'"
62
+ File.open(destination_file, 'w') do |dst|
63
+ dst.write(open(url).read)
64
+ end
65
+ end
66
+
67
+ end
68
+ end
69
+
70
+
@@ -0,0 +1,32 @@
1
+ module LocalchI18n
2
+ class Translator
3
+ # Subclass this class in order to support another translation service
4
+ class Service
5
+
6
+ # uses to_lang
7
+ def initialize options = {}
8
+ configure!
9
+ end
10
+
11
+ def translate text, locale
12
+ text.translate(locale)
13
+ end
14
+
15
+ protected
16
+
17
+ def configure!
18
+ load_service!
19
+ use_key!
20
+ end
21
+
22
+ def load_service!
23
+ require 'to_lang'
24
+ end
25
+
26
+ def use_key!
27
+ ToLang.start(I18n.google_translation_key)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
@@ -0,0 +1,61 @@
1
+ require 'localch_i18n/translator/service'
2
+
3
+ module LocalchI18n
4
+ # Subclass this class in order to support another translation service
5
+ class Translator
6
+ attr_accessor :options
7
+
8
+ def initialize options = {}
9
+ @options = options
10
+ end
11
+
12
+ # TODO: Refactor to use inject
13
+ def auto_translate(flat_hash)
14
+ raise '#auto_translate method requires a #current_locale method in the same module' unless respond_to? :current_locale
15
+
16
+ translated_hash = {}
17
+ flat_hash.each do |key, text|
18
+ translated_hash[key] = translate_it text, current_locale
19
+ end
20
+ translated_hash
21
+ end
22
+
23
+ def translate_it text, locale
24
+ text_has_args? ? translate_with_args(text) : text.translate(current_locale)
25
+ end
26
+
27
+ def service= service
28
+ raise ArgumentError, "Must be a subclass of LocalchI18n::TranslationService, was #{service}" unless service.kind_of?(LocalchI18n::TranslationService)
29
+ @service = service
30
+ end
31
+
32
+ protected
33
+
34
+ def service
35
+ @service ||= LocalchI18n::TranslationService.new options[:service]
36
+ end
37
+
38
+ # split out args parts and pure text parts
39
+ # translate non-arg parts and use arg parts "as is", while re-assembling
40
+ def translate_with_args text, locale
41
+ parts = text.split /(%\{\w+\})/
42
+ parts.inject("") do |res, part|
43
+ res << var_translate(part, locale)
44
+ end
45
+ end
46
+
47
+ # translate only non-arg parts
48
+ def var_translate text, locale
49
+ is_var?(text) ? text : service.translate(text, locale)
50
+ end
51
+
52
+ # is it a variable part?
53
+ def is_var? text
54
+ text =~ /%\{\w+\}/
55
+ end
56
+
57
+ def text_has_args? text
58
+ text =~ /%\{/
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,64 @@
1
+ module LocalchI18n
2
+ module Util
3
+ def load_translations_for input_file, locale
4
+ translations = {}
5
+ translations = YAML.load_file(input_file) if File.exists?(input_file)
6
+
7
+ # Hack to fix "bug" when 'no' for Norway encountered.
8
+ # Parser turns it into false as the key
9
+ no = translations[false]
10
+ translations['no'] = no
11
+
12
+ puts " No translations found!" and return if translations.empty?
13
+ puts " Missing or bad translations root key:" and return if !translations[locale]
14
+ translations[locale]
15
+ end
16
+
17
+ def row_to_hash(key, value)
18
+ res = {}
19
+ keys = key.split('.')
20
+ keys << value
21
+ h = keys.reverse.inject(res) do |a, n|
22
+ if n != keys.last
23
+ { n => a }
24
+ else
25
+ n
26
+ end
27
+ end
28
+ end
29
+
30
+ # options:
31
+ # - parent_key = []
32
+ # - auto_translate
33
+ def flatten_translations_hash(translations, options = {:parent_key => [] })
34
+ flat_hash = {}
35
+ parent_key = options[:parent_key] || []
36
+ translations.each do |key, t|
37
+ current_key = parent_key.dup << key
38
+ if t.is_a?(Hash)
39
+ # descend
40
+ options ||= {}
41
+ options.merge!(:parent_key => current_key)
42
+ flat_hash.merge!(flatten_translations_hash(t, options))
43
+ else
44
+ # leaf -> store as value for key
45
+ flat_hash[current_key.join('.')] = t
46
+ end
47
+ end
48
+ if options[:auto_translate] || auto_translate?
49
+ translator.auto_translate(flat_hash)
50
+ else
51
+ flat_hash
52
+ end
53
+ end
54
+
55
+ def auto_translate?
56
+ false
57
+ end
58
+
59
+ attr_accessor :translator
60
+ def translator
61
+ @translator ||= LocalchI18n::Translator.new
62
+ end
63
+ end
64
+ end