i18n_googledocs 0.1.4

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.
@@ -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