babelish_rnc 1.0.0.pre

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.
Files changed (76) hide show
  1. checksums.yaml +7 -0
  2. data/.babelish.sample +35 -0
  3. data/.gitignore +34 -0
  4. data/.hound.yml +4 -0
  5. data/.travis.yml +15 -0
  6. data/CONTRIBUTING.md +12 -0
  7. data/Dockerfile +6 -0
  8. data/Gemfile +8 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +73 -0
  11. data/Rakefile +17 -0
  12. data/babelish.gemspec +41 -0
  13. data/babelish_rnc.gemspec +42 -0
  14. data/bin/babelish_rnc +6 -0
  15. data/lib/babelish_rnc.rb +31 -0
  16. data/lib/babelish_rnc/android2csv.rb +28 -0
  17. data/lib/babelish_rnc/base2csv.rb +103 -0
  18. data/lib/babelish_rnc/commandline.rb +188 -0
  19. data/lib/babelish_rnc/csv2android.rb +53 -0
  20. data/lib/babelish_rnc/csv2base.rb +189 -0
  21. data/lib/babelish_rnc/csv2json.rb +21 -0
  22. data/lib/babelish_rnc/csv2php.rb +25 -0
  23. data/lib/babelish_rnc/csv2strings.rb +34 -0
  24. data/lib/babelish_rnc/google_doc.rb +55 -0
  25. data/lib/babelish_rnc/json2csv.rb +20 -0
  26. data/lib/babelish_rnc/language.rb +30 -0
  27. data/lib/babelish_rnc/php2csv.rb +30 -0
  28. data/lib/babelish_rnc/strings2csv.rb +61 -0
  29. data/lib/babelish_rnc/version.rb +3 -0
  30. data/lib/babelish_rnc/xcode_macros.rb +49 -0
  31. data/test/babelish_rnc/commands/test_command_android2csv.rb +60 -0
  32. data/test/babelish_rnc/commands/test_command_csv2android.rb +35 -0
  33. data/test/babelish_rnc/commands/test_command_csv2strings.rb +139 -0
  34. data/test/babelish_rnc/commands/test_command_strings2csv.rb +110 -0
  35. data/test/babelish_rnc/test_android2csv.rb +53 -0
  36. data/test/babelish_rnc/test_base2csv.rb +43 -0
  37. data/test/babelish_rnc/test_bins.rb +32 -0
  38. data/test/babelish_rnc/test_commandline.rb +127 -0
  39. data/test/babelish_rnc/test_csv2android.rb +72 -0
  40. data/test/babelish_rnc/test_csv2base.rb +44 -0
  41. data/test/babelish_rnc/test_csv2json.rb +27 -0
  42. data/test/babelish_rnc/test_csv2php.rb +27 -0
  43. data/test/babelish_rnc/test_csv2strings.rb +154 -0
  44. data/test/babelish_rnc/test_json2csv.rb +34 -0
  45. data/test/babelish_rnc/test_php2csv.rb +73 -0
  46. data/test/babelish_rnc/test_strings2csv.rb +147 -0
  47. data/test/babelish_rnc/test_xcode_macros.rb +112 -0
  48. data/test/data/android-en.xml +9 -0
  49. data/test/data/android-fr.xml +9 -0
  50. data/test/data/android.xml +9 -0
  51. data/test/data/android_special_chars.csv +8 -0
  52. data/test/data/android_special_chars.xml +12 -0
  53. data/test/data/android_special_chars_test_result.xml +10 -0
  54. data/test/data/genstrings.strings +0 -0
  55. data/test/data/json.json +6 -0
  56. data/test/data/php_lang.php +8 -0
  57. data/test/data/test_comments.strings +2 -0
  58. data/test/data/test_data.csv +3 -0
  59. data/test/data/test_data.strings +2 -0
  60. data/test/data/test_data_fr_with_comments.strings +6 -0
  61. data/test/data/test_data_fr_with_comments.xml +9 -0
  62. data/test/data/test_data_multiple_langs.csv +3 -0
  63. data/test/data/test_data_with_comments.csv +3 -0
  64. data/test/data/test_data_with_percent.csv +3 -0
  65. data/test/data/test_data_with_percent_space.csv +4 -0
  66. data/test/data/test_data_with_semicolon.csv +3 -0
  67. data/test/data/test_data_with_spaces.csv +3 -0
  68. data/test/data/test_en.strings +2 -0
  69. data/test/data/test_fr.strings +2 -0
  70. data/test/data/test_space.strings +10 -0
  71. data/test/data/test_utf16.strings +0 -0
  72. data/test/data/test_with_nil.csv +3 -0
  73. data/test/data/test_with_nil.strings +4 -0
  74. data/test/data/xcode_empty.strings +7 -0
  75. data/test/test_helper.rb +17 -0
  76. metadata +311 -0
@@ -0,0 +1,188 @@
1
+ require 'thor'
2
+ require 'yaml'
3
+ class Commandline < Thor
4
+ include Thor::Actions
5
+ class_option :verbose, :type => :boolean
6
+ class_option :config, :type => :string, :aliases => "-c", :desc => "Read configuration from given file", :default => ".babelish"
7
+ map "-v" => :version
8
+
9
+ CSVCLASSES = [
10
+ {:name => "CSV2Strings", :ext => ".strings"},
11
+ {:name => "CSV2Android", :ext => ".xml"},
12
+ {:name => "CSV2JSON", :ext => ".json"},
13
+ {:name => "CSV2Php", :ext => ".php"},
14
+ ]
15
+
16
+ CSVCLASSES.each do |klass|
17
+ desc "#{klass[:name].downcase}", "Convert CSV file to #{klass[:ext]}"
18
+ method_option :filename, :type => :string, :aliases => "-i", :desc => "CSV file to convert from or name of file in Google Drive"
19
+ method_option :langs, :type => :hash, :aliases => "-L", :desc => "Languages to convert. i.e. English:en"
20
+
21
+ # optional options
22
+ method_option :excluded_states, :type => :array, :aliases => "-x", :desc => "Exclude rows with given state"
23
+ method_option :state_column, :type => :numeric, :aliases => "-s", :desc => "Position of column for state if any"
24
+ method_option :keys_column, :type => :numeric, :aliases => "-k", :desc => "Position of column for keys"
25
+ method_option :comments_column, :type => :numeric, :aliases => "-C", :desc => "Position of column for comments if any"
26
+ method_option :default_lang, :type => :string, :aliases => "-l", :desc => "Default language to use for empty values if any"
27
+ method_option :csv_separator, :type => :string, :aliases => "-S", :desc => "CSV column separator character, uses ',' by default"
28
+ method_option :output_dir, :type => :string, :aliases => "-d", :desc => "Path of output files"
29
+ method_option :output_basenames, :type => :array, :aliases => "-o", :desc => "Basename of output files"
30
+ method_option :stripping, :type => :boolean, :aliases => "-N", :default => false, :desc => "Strips values of spreadsheet"
31
+ method_option :ignore_lang_path, :type => :boolean, :aliases => "-I", :lazy_default => false, :desc => "Ignore the path component of langs"
32
+ method_option :fetch, :type => :boolean, :desc => "Download file from Google Drive"
33
+ method_option :sheet, :type => :numeric, :desc => "Index of worksheet to download. First index is 0"
34
+ if klass[:name] == "CSV2Strings"
35
+ method_option :macros_filename, :type => :boolean, :aliases => "-m", :lazy_default => false, :desc => "Filename containing defines of localized keys"
36
+ end
37
+ define_method("#{klass[:name].downcase}") do
38
+ csv2base(klass[:name])
39
+ end
40
+ end
41
+
42
+ BASECLASSES = [
43
+ {:name => "Strings2CSV", :ext => ".strings"},
44
+ {:name => "Android2CSV", :ext => ".xml"},
45
+ {:name => "JSON2CSV", :ext => ".json"},
46
+ {:name => "Php2CSV", :ext => ".php"},
47
+ ]
48
+
49
+ BASECLASSES.each do |klass|
50
+ desc "#{klass[:name].downcase}", "Convert #{klass[:ext]} files to CSV file"
51
+ method_option :filenames, :type => :array, :aliases => "-i", :desc => "location of strings files (FILENAMES)"
52
+
53
+ # optional options
54
+ method_option :csv_filename, :type => :string, :aliases => "-o", :desc => "location of output file"
55
+ method_option :headers, :type => :array, :aliases => "-h", :desc => "override headers of columns, default is name of input files and 'Variables' for reference"
56
+ method_option :dryrun, :type => :boolean, :aliases => "-n", :desc => "prints out content of hash without writing file"
57
+ define_method("#{klass[:name].downcase}") do
58
+ begin
59
+ base2csv(klass[:name])
60
+ rescue Errno::ENOENT => e
61
+ warn e.message
62
+ end
63
+ end
64
+ end
65
+
66
+ desc "csv_download", "Download Google Spreadsheet containing translations"
67
+ method_option :gd_filename, :type => :string, :desc => "File to download from Google Drive."
68
+ method_option :sheet, :type => :numeric, :desc => "Index of worksheet to download. First index is 0."
69
+ method_option :all, :type => :boolean, :lazy_default => true, :desc => "Download all worksheets to individual csv files."
70
+ method_option :output_filename, :type => :string, :desc => "Filepath of downloaded file."
71
+ def csv_download
72
+ all = options[:sheet] ? false : options[:all]
73
+ filename = options['gd_filename']
74
+ raise ArgumentError.new("csv_download command : missing file to download") unless filename
75
+ if all
76
+ download(filename)
77
+ else
78
+ download(filename, options['output_filename'], options['sheet'])
79
+ end
80
+ end
81
+
82
+ desc "open FILE", "Open local csv file in default editor or Google Spreadsheet containing translations in default browser"
83
+ def open(file = "translations.csv")
84
+ filename = file || options["filename"]
85
+ if File.exist?(filename)
86
+ say "Opening local file '#{filename}'"
87
+ system "open \"#{filename}\""
88
+ else
89
+ say "Opening Google Drive file '#{filename}'"
90
+ gd = BabelishRnc::GoogleDoc.new
91
+ gd.open filename.to_s
92
+ end
93
+ end
94
+
95
+ desc "init", "Create a configuration file from template"
96
+ def init
97
+ if File.exist?(".babelish")
98
+ say "Config file '.babelish' already exists."
99
+ else
100
+ say "Creating new config file '.babelish'."
101
+ config_file = File.expand_path("../../../.babelish.sample", __FILE__)
102
+ if File.exist?(config_file)
103
+ system "cp #{config_file} .babelish"
104
+ else
105
+ say "Template '#{config_file}' not found."
106
+ end
107
+ end
108
+ end
109
+
110
+
111
+ desc "version", "Display current version"
112
+ def version
113
+ require "babelish_rnc/version"
114
+ say "babelish_rnc #{BabelishRnc::VERSION}"
115
+ end
116
+
117
+ no_tasks do
118
+ def download(filename, output_filename = nil, worksheet_index = nil)
119
+ gd = BabelishRnc::GoogleDoc.new
120
+ if output_filename || worksheet_index
121
+ file_path = gd.download_spreadsheet filename.to_s, output_filename, worksheet_index
122
+ files = [file_path].compact
123
+ else
124
+ files = gd.download filename.to_s
125
+ file_path = files.join("\n") unless files.empty?
126
+ end
127
+
128
+ if file_path
129
+ say "File '#{filename}' downloaded to :\n#{file_path.to_s}"
130
+ else
131
+ say "Could not download the requested file: #{filename}"
132
+ end
133
+ files
134
+ end
135
+
136
+ def csv2base(classname)
137
+ args = options.dup
138
+ if options[:fetch]
139
+ say "Fetching csv file #{options[:filename]} from Google Drive"
140
+ files = download(options[:filename], nil, options[:sheet])
141
+ abort if files.empty? # no file downloaded
142
+ args.delete(:fetch)
143
+ else
144
+ files = [options[:filename]]
145
+ end
146
+ args.delete(:langs)
147
+ args.delete(:filename)
148
+
149
+ xcode_macros = BabelishRnc::XcodeMacros.new if options[:macros_filename]
150
+ files.each_with_index do |filename, index|
151
+ if options[:output_basenames]
152
+ args[:output_basename] = options[:output_basenames][index]
153
+ end
154
+
155
+ class_object = eval "BabelishRnc::#{classname}"
156
+ args = Thor::CoreExt::HashWithIndifferentAccess.new(args)
157
+ converter = class_object.new(filename, options[:langs], args)
158
+ say converter.convert
159
+ xcode_macros.process(converter.table, converter.keys, converter.comments) if options[:macros_filename]
160
+ end
161
+ if options[:macros_filename]
162
+ say "generating macros"
163
+ xcode_macros.write_content(options[:macros_filename])
164
+ end
165
+ end
166
+
167
+ def base2csv(classname)
168
+ class_object = eval "BabelishRnc::#{classname}"
169
+ converter = class_object.new(options)
170
+
171
+ debug_values = converter.convert(!options[:dryrun])
172
+ say debug_values.inspect if options[:dryrun]
173
+ end
174
+ end
175
+
176
+ def self.exit_on_failure?
177
+ true
178
+ end
179
+
180
+ private
181
+
182
+ def options
183
+ original_options = super
184
+ return original_options unless File.exist?(original_options["config"])
185
+ defaults = ::YAML.load_file(original_options["config"]) || {}
186
+ Thor::CoreExt::HashWithIndifferentAccess.new(defaults.merge(original_options))
187
+ end
188
+ end
@@ -0,0 +1,53 @@
1
+ module BabelishRnc
2
+ class CSV2Android < Csv2Base
3
+ attr_accessor :file_path
4
+
5
+ def initialize(filename, langs, args = {})
6
+ super(filename, langs, args)
7
+
8
+ @file_path = args[:output_dir].to_s
9
+ @output_basename = args[:output_basename].to_s
10
+ end
11
+
12
+ def language_filepaths(language)
13
+ require 'pathname'
14
+ output_name = "strings.xml"
15
+ output_name = "#{@output_basename}.xml" unless @output_basename.empty?
16
+ filepath = Pathname.new(@file_path) + "values-#{language.code}" + output_name
17
+ return filepath ? [filepath] : []
18
+ end
19
+
20
+ def process_value(row_value, default_value)
21
+ value = super(row_value, default_value)
22
+ # if the value begins and ends with a quote we must leave them unescapted
23
+ if value.size > 4 && value[0, 2] == "\\\"" && value[value.size - 2, value.size] == "\\\""
24
+ value[0, 2] = "\""
25
+ value[value.size - 2, value.size] = "\""
26
+ end
27
+ value.to_utf8
28
+ end
29
+
30
+ def get_row_format(row_key, row_value, comment = nil, indentation = 0)
31
+ entry = comment.to_s.empty? ? "" : "\n\t<!-- #{comment} -->\n"
32
+ entry + "\t<string name=\"#{row_key}\">#{row_value}</string>\n"
33
+ end
34
+
35
+ def hash_to_output(content = {})
36
+ output = ''
37
+ if content && content.size > 0
38
+ output += "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
39
+ output += "<resources>\n"
40
+ content.each do |key, value|
41
+ comment = @comments[key]
42
+ output += get_row_format(key, value, comment)
43
+ end
44
+ output += "</resources>\n"
45
+ end
46
+ return output
47
+ end
48
+
49
+ def extension
50
+ "xml"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,189 @@
1
+ require 'pathname'
2
+ module BabelishRnc
3
+ class Csv2Base
4
+ attr_accessor :output_dir, :output_basename
5
+ attr_accessor :ignore_lang_path
6
+ attr_accessor :langs
7
+ attr_accessor :csv_filename
8
+ attr_accessor :default_lang
9
+ attr_accessor :csv_separator
10
+ attr_accessor :excluded_states, :state_column, :keys_column, :comments_column
11
+ attr_accessor :languages
12
+
13
+ def initialize(filename, langs, args = {})
14
+ default_args = {
15
+ :excluded_states => [],
16
+ :state_column => nil,
17
+ :keys_column => 0,
18
+ :csv_separator => ","
19
+ }
20
+
21
+ args = default_args.merge!(args)
22
+ args = Thor::CoreExt::HashWithIndifferentAccess.new(args)
23
+ @langs = langs
24
+
25
+ # check input types
26
+ raise ArgumentError.new("wrong value of filename: #{filename.inspect}") unless filename.is_a?(String)
27
+ if !@langs.is_a?(Hash) || @langs.size == 0
28
+ raise ArgumentError.new("wrong format or/and langs parameter: #{@langs.inspect}")
29
+ end
30
+
31
+ @output_basename = args[:output_basename]
32
+ @output_dir = args[:output_dir].to_s
33
+ @csv_filename = filename
34
+ @excluded_states = args[:excluded_states]
35
+ @state_column = args[:state_column]
36
+ @keys_column = args[:keys_column]
37
+ @comments_column = args[:comments_column]
38
+ @default_lang = args[:default_lang]
39
+ @csv_separator = args[:csv_separator]
40
+ @ignore_lang_path = args[:ignore_lang_path]
41
+ @stripping = args[:stripping]
42
+ @languages = []
43
+ @comments = {}
44
+ end
45
+
46
+ def create_file_from_path(file_path)
47
+ path = File.dirname(file_path)
48
+ FileUtils.mkdir_p path
49
+ return File.new(file_path, "w")
50
+ end
51
+
52
+ def keys
53
+ @languages.each do |lang|
54
+ next unless lang
55
+ return lang.content.keys unless lang.content.keys.empty?
56
+ end
57
+ return []
58
+ end
59
+
60
+ def table
61
+ output_basename
62
+ end
63
+
64
+ def comments
65
+ @comments
66
+ end
67
+
68
+ def language_filepaths(language)
69
+ #implement in subclass
70
+ []
71
+ end
72
+
73
+ def extension
74
+ #implement in subclass
75
+ ""
76
+ end
77
+
78
+ def default_filepath
79
+ Pathname.new(@output_dir) + "#{output_basename}.#{extension}"
80
+ end
81
+
82
+ def process_value(row_value, default_value)
83
+ value = row_value.nil? ? default_value : row_value
84
+ value = "" if value.nil?
85
+ value.gsub!(/\\*\"/, "\\\"") # escape double quotes
86
+ value.gsub!(/\n/, "\\n") # replace new lines with \n, dont strip
87
+ value.strip! if @stripping
88
+ return value.to_utf8
89
+ end
90
+
91
+ def get_row_format(row_key, row_value, comment = nil, indentation = 0)
92
+ # ignoring comment by default
93
+ "\"#{row_key}\"" + " " * indentation + " = \"#{row_value}\""
94
+ end
95
+
96
+ # Convert csv file to multiple Localizable.strings files for each column
97
+ def convert(name = @csv_filename)
98
+ rowIndex = 0
99
+ excludedCols = []
100
+ defaultCol = 0
101
+
102
+ CSV.foreach(name, :quote_char => '"', :col_sep => @csv_separator, :row_sep => :auto) do |row|
103
+
104
+ if rowIndex == 0
105
+ #check there's at least two columns
106
+ return unless row.count > 1
107
+ else
108
+ #skip empty lines (or sections)
109
+ next if row == nil or row[@keys_column].nil?
110
+ end
111
+
112
+ # go through columns
113
+ row.size.times do |i|
114
+ next if excludedCols.include? i
115
+
116
+ #header
117
+ if rowIndex == 0
118
+ # defaultCol can be the keyValue
119
+ defaultCol = i if self.default_lang == row[i]
120
+ # ignore all headers not listed in langs to create files
121
+ (excludedCols << i and next) unless @langs.has_key?(row[i])
122
+
123
+ language = Language.new(row[i])
124
+ if @langs[row[i]].is_a?(Array)
125
+ @langs[row[i]].each do |id|
126
+ language.add_language_id(id.to_s)
127
+ end
128
+ else
129
+ language.add_language_id(@langs[row[i]].to_s)
130
+ end
131
+ @languages[i] = language
132
+ elsif !@state_column || (row[@state_column].nil? || row[@state_column] == '' || !@excluded_states.include?(row[@state_column]))
133
+ key = row[@keys_column]
134
+ comment = @comments_column ? row[@comments_column] : nil
135
+ key.strip! if @stripping
136
+ default_value = self.default_lang ? row[defaultCol] : nil
137
+ value = self.process_value(row[i], default_value)
138
+ @comments[key] = comment
139
+ @languages[i].add_content_pair(key, value)
140
+ end
141
+ end
142
+
143
+ rowIndex += 1
144
+ end
145
+
146
+ write_content
147
+ end
148
+
149
+ def write_content
150
+ info = "List of created files:\n"
151
+ count = 0
152
+ @languages.each do |language|
153
+ next if language.nil?
154
+
155
+ files = []
156
+ if @ignore_lang_path
157
+ files << create_file_from_path(default_filepath)
158
+ else
159
+ language_filepaths(language).each do |filename|
160
+ files << create_file_from_path(filename)
161
+ end
162
+ end
163
+ files.each do |file|
164
+ file.write hash_to_output(language.content)
165
+ info += "- #{File.absolute_path(file)}\n"
166
+ count += 1
167
+
168
+ file.close
169
+ end
170
+ end
171
+
172
+ info = "Created #{count} files.\n" + info
173
+ return info
174
+ end
175
+
176
+ def hash_to_output(content = {})
177
+ output = ''
178
+ indentation = content.map(&:first).max { |a, b| a.length <=> b.length }.length
179
+ if content && content.size > 0
180
+ content.each do |key, value|
181
+ comment = @comments[key]
182
+ defaultlang = @languages[1].content[key]
183
+ output += get_row_format(key, value, comment, indentation - key.length, defaultlang)
184
+ end
185
+ end
186
+ return output
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,21 @@
1
+ module BabelishRnc
2
+ require 'json'
3
+ class CSV2JSON < Csv2Base
4
+
5
+ def language_filepaths(language)
6
+ require 'pathname'
7
+ filename = @output_basename || language.code
8
+ filepath = Pathname.new("#{@output_dir}#{filename}.json")
9
+
10
+ return filepath ? [filepath] : []
11
+ end
12
+
13
+ def hash_to_output(content = {})
14
+ return content.to_json
15
+ end
16
+
17
+ def extension
18
+ "js"
19
+ end
20
+ end
21
+ end