csv2strings 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://secure.travis-ci.org/netbe/CSV-to-iOS-Localizable.strings-converter.png?branch=master)](http://travis-ci.org/netbe/CSV-to-iOS-Localizable.strings-converter)
|
2
2
|
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/netbe/CSV-to-iOS-Localizable.strings-converter)
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/netbe/CSV-to-iOS-Localizable.strings-converter/badge.png)](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
|