csv2strings 0.2.1 → 0.2.2
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.
- data/.gitignore +20 -4
- data/.travis.yml +12 -4
- data/Gemfile +5 -1
- data/README.md +3 -6
- data/Rakefile +4 -2
- data/bin/csv2strings +2 -2
- data/bin/strings2csv +2 -2
- data/csv2strings.gemspec +5 -3
- data/lib/csvconverter.rb +11 -6
- data/lib/{command.rb → csvconverter/command.rb} +0 -3
- data/lib/{csv2strings_command.rb → csvconverter/commands/csv2strings_command.rb} +5 -6
- data/lib/{strings2csv_command.rb → csvconverter/commands/strings2csv_command.rb} +5 -7
- data/lib/csvconverter/csv2strings.rb +132 -0
- data/lib/{google_doc.rb → csvconverter/google_doc.rb} +13 -4
- data/lib/csvconverter/strings2csv.rb +96 -0
- data/test/csvconverter/commands/test_command_csv2strings.rb +36 -0
- data/test/csvconverter/commands/test_command_strings2csv.rb +77 -0
- data/test/{csv2strings/converter_test.rb → csvconverter/test_csv2strings.rb} +5 -7
- data/test/{strings2csv/converter_test.rb → csvconverter/test_strings2csv.rb} +26 -22
- data/test/data/test_with_nil.csv +3 -0
- data/test/data/test_with_nil.strings +4 -0
- data/test/test_helper.rb +8 -2
- metadata +152 -113
- data/Gemfile.lock +0 -49
- data/lib/csv2strings/converter.rb +0 -133
- data/lib/strings2csv/converter.rb +0 -95
- data/test/command_test.rb +0 -90
- data/test/google_doc_test.rb +0 -6
data/.gitignore
CHANGED
@@ -1,11 +1,27 @@
|
|
1
|
+
# Generated files
|
1
2
|
*.csv
|
2
3
|
*.strings
|
3
4
|
*.lproj
|
4
|
-
|
5
|
+
*.gem
|
5
6
|
*~
|
6
7
|
*#
|
7
|
-
*.gem
|
8
|
-
*.sublime-project
|
9
8
|
#*#
|
9
|
+
|
10
|
+
# Config file
|
10
11
|
.csvconverter
|
11
|
-
|
12
|
+
|
13
|
+
# SimpleCov
|
14
|
+
coverage
|
15
|
+
|
16
|
+
# ignore Gemfile.lock as http://yehudakatz.com/2010/12/16/clarifying-the-roles-of-the-gemspec-and-gemfile/
|
17
|
+
# this should solve the fastercsv issue
|
18
|
+
Gemfile.lock
|
19
|
+
|
20
|
+
# Ruby package manager files
|
21
|
+
.ruby-version
|
22
|
+
.rbenv-version
|
23
|
+
.rvmrc
|
24
|
+
|
25
|
+
# Sublime Text
|
26
|
+
*.sublime-workspace
|
27
|
+
*.sublime-project
|
data/.travis.yml
CHANGED
@@ -1,6 +1,14 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
- 1.9.3
|
4
|
+
- 1.9.2
|
5
|
+
- 1.8.7
|
6
|
+
- 2.0.0
|
7
|
+
deploy:
|
8
|
+
provider: rubygems
|
9
|
+
api_key:
|
10
|
+
secure: Xjq+v+jEU6wK4BtyfnV1elegNcxK6Ah/O99Sn9c2IlkCmJ1wxLBouqzEiSorSJ4IOMa5H2y3gwo5GXOr6Y7d8huyGrPuBeCSGqAmH77wNCIv7G+jnLiYb1sRZbtKcPW2QaN6JF81qDIelwyspMfo6/ug1qN1x323UaxZl7f7nUE=
|
11
|
+
gem: csv2strings
|
12
|
+
on:
|
13
|
+
tags: true
|
14
|
+
repo: netbe/CSV-to-iOS-Localizable.strings-converter
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
[](http://travis-ci.org/netbe/CSV-to-iOS-Localizable.strings-converter)
|
2
2
|
[](https://codeclimate.com/github/netbe/CSV-to-iOS-Localizable.strings-converter)
|
3
|
+
[](https://coveralls.io/r/netbe/CSV-to-iOS-Localizable.strings-converter)
|
3
4
|
# Introduction
|
4
5
|
This script converts a csv file of translations into iOS .strings files and vice-versa.
|
5
6
|
|
@@ -33,10 +34,6 @@ Edge version can be found on `develop` branch.
|
|
33
34
|
|
34
35
|
Run `bundle install` to install all the dependencies. Tests are done with `Test::Unit` so run `rake test` to run all the test suite.
|
35
36
|
|
36
|
-
# Todo
|
37
|
+
# Todo & Known issues
|
37
38
|
|
38
|
-
See GitHub
|
39
|
-
|
40
|
-
# Known issues
|
41
|
-
|
42
|
-
None
|
39
|
+
See GitHub issues
|
data/Rakefile
CHANGED
@@ -2,8 +2,10 @@ require 'rake/testtask'
|
|
2
2
|
|
3
3
|
Rake::TestTask.new do |t|
|
4
4
|
t.libs << "test"
|
5
|
-
t.test_files = FileList['test
|
5
|
+
t.test_files = FileList['test/csvconverter/**/test_*.rb']
|
6
|
+
# t.warning = true
|
7
|
+
t.verbose = true
|
6
8
|
end
|
7
9
|
|
8
10
|
desc "Run tests"
|
9
|
-
task :default => :test
|
11
|
+
task :default => :test
|
data/bin/csv2strings
CHANGED
data/bin/strings2csv
CHANGED
data/csv2strings.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'csv2strings'
|
3
|
-
s.version = '0.2.
|
4
|
-
s.date = '2013-10-
|
3
|
+
s.version = '0.2.2'
|
4
|
+
s.date = '2013-10-30'
|
5
5
|
s.summary = "CSV to iOS Localizable.strings converter"
|
6
6
|
s.description = "ruby script converts a CSV file of translations to Localizable.strings files and vice-versa"
|
7
7
|
s.authors = ["François Benaiteau"]
|
@@ -14,8 +14,10 @@ Gem::Specification.new do |s|
|
|
14
14
|
|
15
15
|
if RUBY_VERSION < '1.9'
|
16
16
|
s.add_dependency "fastercsv"
|
17
|
+
s.add_dependency "nokogiri", "= 1.5.10"
|
18
|
+
s.add_dependency "orderedhash"
|
17
19
|
end
|
18
|
-
|
20
|
+
|
19
21
|
s.add_dependency "google_drive", '0.3.6'
|
20
22
|
s.add_development_dependency "rake"
|
21
23
|
|
data/lib/csvconverter.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
|
-
$: << File.expand_path(File.join(File.dirname(__FILE__)))
|
2
|
-
require 'rubygems'
|
3
|
-
|
4
1
|
CSVGEM = RUBY_VERSION.match(/^[0-1]\.[0-8]\./) ? 'faster_csv' : 'csv'
|
5
2
|
|
3
|
+
if RUBY_VERSION.match(/^[0-1]\.[0-8]\./)
|
4
|
+
require "orderedhash"
|
5
|
+
ORDERED_HASH_CLASS = OrderedHash
|
6
|
+
else
|
7
|
+
ORDERED_HASH_CLASS = Hash
|
8
|
+
end
|
9
|
+
|
6
10
|
begin
|
7
11
|
require CSVGEM
|
8
12
|
rescue LoadError
|
9
13
|
puts "Failed to load #{CSVGEM} (ruby #{RUBY_VERSION})"
|
10
14
|
puts "gem install #{CSVGEM}"
|
11
|
-
|
15
|
+
abort
|
12
16
|
end
|
13
17
|
|
14
18
|
CSVParserClass = CSVGEM == 'csv' ? CSV : FasterCSV
|
15
|
-
require "csv2strings
|
16
|
-
require "strings2csv
|
19
|
+
require "csvconverter/csv2strings"
|
20
|
+
require "csvconverter/strings2csv"
|
21
|
+
require "csvconverter/google_doc"
|
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
require "command"
|
1
|
+
require "csvconverter/command"
|
3
2
|
class CSV2StringsCommand < Command
|
4
3
|
default_task :csv2strings
|
5
4
|
|
@@ -20,7 +19,7 @@ class CSV2StringsCommand < Command
|
|
20
19
|
help("csv2strings")
|
21
20
|
exit
|
22
21
|
end
|
23
|
-
|
22
|
+
|
24
23
|
filename ||= options['filename']
|
25
24
|
if options['fetch']
|
26
25
|
say "Downloading file from Google Drive"
|
@@ -33,12 +32,12 @@ class CSV2StringsCommand < Command
|
|
33
32
|
help("csv2strings")
|
34
33
|
exit
|
35
34
|
end
|
36
|
-
|
35
|
+
|
37
36
|
args = options.dup
|
38
37
|
args.delete(:langs)
|
39
38
|
args.delete(:filename)
|
40
|
-
converter = CSV2Strings
|
41
|
-
say converter.csv_to_dotstrings
|
39
|
+
converter = CSV2Strings.new(filename, options[:langs], args)
|
40
|
+
say converter.csv_to_dotstrings
|
42
41
|
end
|
43
42
|
|
44
43
|
end
|
@@ -1,6 +1,4 @@
|
|
1
|
-
|
2
|
-
require "command"
|
3
|
-
|
1
|
+
require "csvconverter/command"
|
4
2
|
class Strings2CSVCommand < Command
|
5
3
|
default_task :strings2csv
|
6
4
|
|
@@ -15,10 +13,10 @@ class Strings2CSVCommand < Command
|
|
15
13
|
unless options.has_key?('filenames')
|
16
14
|
say "No value provided for required options '--filenames'"
|
17
15
|
help("strings2csv")
|
18
|
-
|
16
|
+
return
|
19
17
|
end
|
20
|
-
converter = Strings2CSV
|
18
|
+
converter = Strings2CSV.new(options)
|
21
19
|
debug_values = converter.dotstrings_to_csv(!options[:dryrun])
|
22
|
-
say debug_values.inspect if options[:dryrun]
|
20
|
+
say debug_values.inspect if options[:dryrun]
|
23
21
|
end
|
24
|
-
end
|
22
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
class CSV2Strings
|
2
|
+
attr_accessor :csv_filename, :output_file
|
3
|
+
attr_accessor :langs, :default_lang
|
4
|
+
attr_accessor :default_path
|
5
|
+
attr_accessor :excluded_states, :state_column, :keys_column
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(filename, langs, args = {})
|
9
|
+
args.merge!({
|
10
|
+
:excluded_states => [],
|
11
|
+
:state_column => nil,
|
12
|
+
:keys_column => 0})
|
13
|
+
|
14
|
+
@csv_filename = filename
|
15
|
+
@langs = langs
|
16
|
+
|
17
|
+
if !@langs.is_a?(Hash) || @langs.size == 0
|
18
|
+
raise "wrong format or/and languages parameter" + @langs.inspect
|
19
|
+
end
|
20
|
+
@output_file = (@langs.size == 1) ? args[:output_file] : nil
|
21
|
+
|
22
|
+
@default_path = args[:default_path].to_s
|
23
|
+
@excluded_states = args[:excluded_states]
|
24
|
+
@state_column = args[:state_column]
|
25
|
+
@keys_column = args[:keys_column]
|
26
|
+
@default_lang = args[:default_lang]
|
27
|
+
end
|
28
|
+
|
29
|
+
def create_file_from_path(file_path)
|
30
|
+
path = File.dirname(file_path)
|
31
|
+
FileUtils.mkdir_p path
|
32
|
+
return File.new(file_path,"w")
|
33
|
+
end
|
34
|
+
|
35
|
+
def process_header(excludedCols, files, row, index)
|
36
|
+
files[index] = []
|
37
|
+
lang_index = row[index]
|
38
|
+
|
39
|
+
# create output files here
|
40
|
+
if @output_file
|
41
|
+
# one single file
|
42
|
+
files[index] << self.create_file_from_path(@output_file)
|
43
|
+
else
|
44
|
+
# create one file for each languages
|
45
|
+
if self.langs[lang_index].is_a?(Array)
|
46
|
+
|
47
|
+
self.langs[lang_index].each do |locale|
|
48
|
+
filename = self.file_path_for_locale(locale)
|
49
|
+
files[index] << self.create_file_from_path(filename)
|
50
|
+
end
|
51
|
+
elsif self.langs[lang_index].is_a?(String)
|
52
|
+
locale = self.langs[lang_index]
|
53
|
+
filename = self.file_path_for_locale(locale)
|
54
|
+
files[index] << self.create_file_from_path(filename)
|
55
|
+
else
|
56
|
+
raise "wrong format or/and languages parameter"
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def file_path_for_locale(locale)
|
63
|
+
require 'pathname'
|
64
|
+
Pathname.new(self.default_path) + "#{locale}.lproj" + "Localizable.strings"
|
65
|
+
end
|
66
|
+
|
67
|
+
def process_value(row_value, default_value)
|
68
|
+
value = row_value.nil? ? default_value : row_value
|
69
|
+
value = "" if value.nil?
|
70
|
+
value.gsub!(/\\*\"/, "\\\"") #escape double quotes
|
71
|
+
value.gsub!(/\s*(\n|\\\s*n)\s*/, "\\n") #replace new lines with \n + strip
|
72
|
+
value.gsub!(/%\s+([a-zA-Z@])([^a-zA-Z@]|$)/, "%\\1\\2") #repair string formats ("% d points" etc)
|
73
|
+
value.gsub!(/([^0-9\s\(\{\[^])%/, "\\1 %")
|
74
|
+
value.strip!
|
75
|
+
return value
|
76
|
+
end
|
77
|
+
|
78
|
+
# Convert csv file to multiple Localizable.strings files for each column
|
79
|
+
def csv_to_dotstrings(name = self.csv_filename)
|
80
|
+
files = {}
|
81
|
+
rowIndex = 0
|
82
|
+
excludedCols = []
|
83
|
+
defaultCol = 0
|
84
|
+
nb_translations = 0
|
85
|
+
|
86
|
+
CSVParserClass.foreach(name, :quote_char => '"', :col_sep =>',', :row_sep => :auto) do |row|
|
87
|
+
|
88
|
+
if rowIndex == 0
|
89
|
+
return unless row.count > 1 #check there's at least two columns
|
90
|
+
else
|
91
|
+
next if row == nil or row[self.keys_column].nil? #skip empty lines (or sections)
|
92
|
+
end
|
93
|
+
|
94
|
+
row.size.times do |i|
|
95
|
+
next if excludedCols.include? i
|
96
|
+
if rowIndex == 0 #header
|
97
|
+
# ignore all headers not listed in langs to create files
|
98
|
+
(excludedCols << i and next) unless self.langs.has_key?(row[i])
|
99
|
+
self.process_header(excludedCols, files, row, i)
|
100
|
+
# define defaultCol
|
101
|
+
defaultCol = i if self.default_lang == row[i]
|
102
|
+
elsif !self.state_column || (row[self.state_column].nil? or row[self.state_column] == '' or !self.excluded_states.include? row[self.state_column])
|
103
|
+
# TODO: add option to strip the constant or referenced language
|
104
|
+
key = row[self.keys_column].strip
|
105
|
+
value = self.process_value(row[i], row[defaultCol])
|
106
|
+
# files for a given language, i.e could group english US with english UK.
|
107
|
+
localized_files = files[i]
|
108
|
+
if localized_files
|
109
|
+
localized_files.each do |file|
|
110
|
+
nb_translations += 1
|
111
|
+
file.write "\"#{key}\" = \"#{value}\";\n"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
rowIndex += 1
|
117
|
+
end
|
118
|
+
info = "Created #{files.size} files. Content: #{nb_translations} translations\n"
|
119
|
+
info += "List of created files:\n"
|
120
|
+
|
121
|
+
# closing I/O
|
122
|
+
files.each do |key,locale_files|
|
123
|
+
locale_files.each do |file|
|
124
|
+
info += "#{file.path.to_s}\n"
|
125
|
+
file.close
|
126
|
+
end
|
127
|
+
end
|
128
|
+
info
|
129
|
+
end # end of method
|
130
|
+
|
131
|
+
end # end of class
|
132
|
+
|
@@ -1,4 +1,13 @@
|
|
1
|
-
|
1
|
+
# Faraday is a dependency of google_drive, this silents the warning
|
2
|
+
# see https://github.com/CocoaPods/CocoaPods/commit/f33f967427b857bf73645fd4d3f19eb05e9be0e0
|
3
|
+
# This is to make sure Faraday doesn't warn the user about the `system_timer` gem missing.
|
4
|
+
old_warn, $-w = $-w, nil
|
5
|
+
begin
|
6
|
+
require "google_drive"
|
7
|
+
ensure
|
8
|
+
$-w = old_warn
|
9
|
+
end
|
10
|
+
|
2
11
|
class GoogleDoc
|
3
12
|
attr_accessor :session
|
4
13
|
|
@@ -11,9 +20,9 @@ class GoogleDoc
|
|
11
20
|
unless @session
|
12
21
|
self.authenticate
|
13
22
|
end
|
14
|
-
result = @session.file_by_title(requested_filename)
|
23
|
+
result = @session.file_by_title(requested_filename)
|
15
24
|
if result.is_a? Array
|
16
|
-
file = result.first
|
25
|
+
file = result.first
|
17
26
|
else
|
18
27
|
file = result
|
19
28
|
end
|
@@ -22,4 +31,4 @@ class GoogleDoc
|
|
22
31
|
return output_filename
|
23
32
|
end
|
24
33
|
|
25
|
-
end
|
34
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
class Strings2CSV
|
2
|
+
# default_lang is the the column to refer to if a value is missing
|
3
|
+
# actually default_lang = default_filename
|
4
|
+
attr_accessor :csv_filename, :headers, :filenames, :default_lang
|
5
|
+
|
6
|
+
def initialize(args = {:filenames => []})
|
7
|
+
raise ArgumentError.new("No filenames given") unless args[:filenames]
|
8
|
+
if args[:headers]
|
9
|
+
raise ArgumentError.new("number of headers and files don't match, don't forget the constant column") unless args[:headers].size == (args[:filenames].size + 1)
|
10
|
+
end
|
11
|
+
|
12
|
+
@filenames = args[:filenames]
|
13
|
+
|
14
|
+
@csv_filename = args[:csv_filename] || "translations.csv"
|
15
|
+
@default_lang = args[:default_lang]
|
16
|
+
@headers = args[:headers] || self.default_headers
|
17
|
+
end
|
18
|
+
|
19
|
+
def default_headers
|
20
|
+
headers = ["Variables"]
|
21
|
+
@filenames.each do |fname|
|
22
|
+
headers << fname
|
23
|
+
end
|
24
|
+
headers
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
# Load all strings of a given file
|
29
|
+
def load_strings(strings_filename)
|
30
|
+
strings = ORDERED_HASH_CLASS.new
|
31
|
+
File.open(strings_filename, 'r') do |strings_file|
|
32
|
+
strings_file.read.each_line do |line|
|
33
|
+
hash = self.parse_dotstrings_line(line)
|
34
|
+
strings.merge!(hash) if hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
strings
|
38
|
+
end
|
39
|
+
|
40
|
+
def parse_dotstrings_line(line)
|
41
|
+
line.strip!
|
42
|
+
if (line[0] != ?# and line[0] != ?=)
|
43
|
+
m = line.match(/^[^\"]*\"(.+)\"[^=]+=[^\"]*\"(.*)\";/)
|
44
|
+
return {m[1] => m[2]} unless m.nil?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# Convert Localizable.strings files to one CSV file
|
50
|
+
# output: strings hash has filename for keys and the content of csv
|
51
|
+
def dotstrings_to_csv(write_to_file = true)
|
52
|
+
# Parse .strings files
|
53
|
+
strings = {}
|
54
|
+
keys = nil
|
55
|
+
lang_order = []
|
56
|
+
|
57
|
+
@filenames.each do |fname|
|
58
|
+
header = fname
|
59
|
+
strings[header] = load_strings(fname)
|
60
|
+
keys ||= strings[header].keys
|
61
|
+
end
|
62
|
+
|
63
|
+
if(write_to_file)
|
64
|
+
# Create csv file
|
65
|
+
puts "Creating #{@csv_filename}"
|
66
|
+
create_csv_file(keys, strings)
|
67
|
+
else
|
68
|
+
return keys, strings
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def basename(file_path)
|
73
|
+
filename = File.basename(file_path)
|
74
|
+
return filename.split('.')[0].to_sym if file_path
|
75
|
+
end
|
76
|
+
|
77
|
+
# Create the resulting file
|
78
|
+
def create_csv_file(keys, strings)
|
79
|
+
raise "csv_filename must not be nil" unless self.csv_filename
|
80
|
+
CSVParserClass.open(self.csv_filename, "wb") do |csv|
|
81
|
+
csv << @headers
|
82
|
+
keys.each do |key|
|
83
|
+
line = [key]
|
84
|
+
default_val = strings[self.default_lang][key] if strings[self.default_lang]
|
85
|
+
@filenames.each do |fname|
|
86
|
+
lang = fname
|
87
|
+
current_val = strings[lang][key]
|
88
|
+
line << ((lang != self.default_lang and current_val == default_val) ? '' : current_val)
|
89
|
+
end
|
90
|
+
csv << line
|
91
|
+
end
|
92
|
+
puts "Done"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|