babelish_rnc 1.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
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