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 CHANGED
@@ -1,11 +1,27 @@
1
+ # Generated files
1
2
  *.csv
2
3
  *.strings
3
4
  *.lproj
4
- .rbenv-version
5
+ *.gem
5
6
  *~
6
7
  *#
7
- *.gem
8
- *.sublime-project
9
8
  #*#
9
+
10
+ # Config file
10
11
  .csvconverter
11
- coverage
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
- - 1.9.3
4
- - 1.9.2
5
- - 1.8.7
6
- - 2.0.0
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
@@ -1,4 +1,8 @@
1
1
  source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in teachmehowtomakearubygem.gemspec
4
- gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'coveralls', :require => false, :platforms => [:ruby_19, :ruby_20]
8
+ end
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 isssues
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/**/*_test.rb']
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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- require File.expand_path('../../lib/csv2strings_command', __FILE__)
3
-
2
+ $: << File.expand_path("../lib/", __FILE__)
3
+ require 'csvconverter/commands/csv2strings_command'
4
4
 
5
5
  CSV2StringsCommand.start
data/bin/strings2csv CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- require File.expand_path('../../lib/strings2csv_command', __FILE__)
3
-
2
+ $: << File.expand_path("../lib/", __FILE__)
3
+ require 'csvconverter/commands/strings2csv_command'
4
4
 
5
5
  Strings2CSVCommand.start
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.1'
4
- s.date = '2013-10-15'
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
- s.add_dependency "nokogiri", "= 1.5.10"
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
- exit
15
+ abort
12
16
  end
13
17
 
14
18
  CSVParserClass = CSVGEM == 'csv' ? CSV : FasterCSV
15
- require "csv2strings/converter"
16
- require "strings2csv/converter"
19
+ require "csvconverter/csv2strings"
20
+ require "csvconverter/strings2csv"
21
+ require "csvconverter/google_doc"
@@ -1,8 +1,5 @@
1
- $: << File.expand_path(File.join(File.dirname(__FILE__)))
2
1
  require 'yaml'
3
2
  require 'thor'
4
- require 'csvconverter'
5
- require 'google_doc'
6
3
 
7
4
  class Command < Thor
8
5
  include Thor::Actions
@@ -1,5 +1,4 @@
1
- $: << File.expand_path(File.join(File.dirname(__FILE__)))
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::Converter.new(filename, options[:langs], args)
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
- $: << File.expand_path(File.join(File.dirname(__FILE__)))
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
- exit
16
+ return
19
17
  end
20
- converter = Strings2CSV::Converter.new(options)
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
- require "google_drive"
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