babelyoda 1.6.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .rvmrc
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg/*
data/CHANGELOG CHANGED
@@ -1,3 +1,4 @@
1
1
  v1.1.0. Initial release.
2
2
  v1.4.0. Better command line options handling + Bugfixes.
3
3
  v1.5.0. Fixed a major bug in localization of XIB files.
4
+ v2.0.0. Full re-write. Yay!
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in babelyoda.gemspec
4
+ gemspec
5
+
6
+
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Andrey Subbotin
1
+ Copyright (c) 2010-2012 Andrey Subbotin
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -1,6 +1,6 @@
1
1
  = babelyoda
2
2
 
3
- A simple utility to push/pull l10n resources of an iPhone project to/from the translators.
3
+ A simple utility to push/pull l10n resources of an Xcode project to/from the translators.
4
4
 
5
5
  == Note on Patches/Pull Requests
6
6
 
@@ -14,4 +14,4 @@ A simple utility to push/pull l10n resources of an iPhone project to/from the tr
14
14
 
15
15
  == Copyright
16
16
 
17
- Copyright (c) 2010 Andrey Subbotin. See LICENSE for details.
17
+ Copyright (c) 2010-2012 Andrey Subbotin. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/babelyoda.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "babelyoda/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "babelyoda"
7
+ s.version = Babelyoda::VERSION
8
+ s.authors = ["Andrey Subbotin"]
9
+ s.email = ["andrey@subbotin.me"]
10
+ s.homepage = "http://github.com/eploko/babelyoda"
11
+ s.summary = "Xcode project localization made easy"
12
+ s.description = "A simple utility to push/pull l10n resources of an Xcode project to/from the translators"
13
+
14
+ s.rubyforge_project = "babelyoda"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.require_paths = ["lib"]
18
+
19
+ # specify any dependencies here; for example:
20
+ s.add_development_dependency "rspec", '~> 2.8', '>= 2.8.0'
21
+ s.add_runtime_dependency "awesome_print", '~> 1.0', '>= 1.0.2'
22
+ s.add_runtime_dependency "rake", '~> 0.9', '>= 0.9.2.2'
23
+ s.add_runtime_dependency "active_support", '~> 3.0', '>= 3.0.0'
24
+ s.add_runtime_dependency "rchardet19", '~> 1.3', '>= 1.3.5'
25
+ s.add_runtime_dependency "builder", '~> 3.0', '>= 3.0.0'
26
+ s.add_runtime_dependency "nokogiri", '~> 1.5', '>= 1.5.0'
27
+ s.add_runtime_dependency "term-ansicolor", '~> 1.0', '>= 1.0.7'
28
+ end
@@ -0,0 +1,20 @@
1
+ class File
2
+ def self.lproj_part(filename)
3
+ filename.split('/').each do |part|
4
+ return part if part =~ /^.*\.lproj$/
5
+ end
6
+ nil
7
+ end
8
+
9
+ def self.omit_lproj(filename)
10
+ File.join(filename.split('/').delete_if { |p| p.match(/^.*\.lproj$/) })
11
+ end
12
+
13
+ def self.localized(filename, language)
14
+ if lproj_part(filename)
15
+ File.join(filename.split('/').map { |p| p.match(/^.*\.lproj$/) ? "#{language}.lproj" : p })
16
+ else
17
+ File.join(File.dirname(filename), "#{language}.lproj", File.basename(filename))
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ require 'fileutils'
2
+ require 'tmpdir'
3
+
4
+ require_relative 'keyset'
5
+ require_relative 'strings'
6
+
7
+ module Babelyoda
8
+ class Genstrings
9
+ def self.run(files = [], language, &block)
10
+ keysets = {}
11
+ files.each do |fn|
12
+ Dir.mktmpdir do |dir|
13
+ raise "ERROR: genstrings failed." unless Kernel.system("genstrings -littleEndian -o '#{dir}' '#{fn}'")
14
+ Dir.glob(File.join(dir, '*.strings')).each do |strings_file|
15
+ strings = Babelyoda::Strings.new(strings_file, language).read!
16
+ strings.name = File.join('Resources', File.basename(strings.name))
17
+ keysets[strings.name] ||= Keyset.new(strings.name)
18
+ keysets[strings.name].merge!(strings)
19
+ end
20
+ end
21
+ end
22
+ keysets.each_value do |item|
23
+ yield(item)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,99 @@
1
+ require_relative 'git_versions'
2
+ require_relative 'logger'
3
+ require_relative 'specification_loader'
4
+
5
+ module Babelyoda
6
+ class Git
7
+ include Babelyoda::SpecificationLoader
8
+
9
+ def version_exist?(filename)
10
+ versions.exist?(filename)
11
+ end
12
+
13
+ def store_version!(filename)
14
+ @versions[filename] = git_ls_sha1(filename)
15
+ should_add = !File.exist?(versions.filename)
16
+ versions.save!
17
+ end
18
+
19
+ def fetch_versions!(*filenames, &block)
20
+ Dir.mktmpdir do |dir|
21
+ results = []
22
+ filenames.each do |fn|
23
+ full_fn = File.join(dir, fn)
24
+ dirname = File.dirname(full_fn)
25
+ FileUtils.mkdir_p dirname
26
+ git_show(@versions[fn], full_fn)
27
+ results << full_fn
28
+ end
29
+ block.call(results)
30
+ end
31
+ end
32
+
33
+ def transaction(msg)
34
+ check_requirements!
35
+ yield if block_given?
36
+ if git_status.size > 0
37
+ git_add!('.')
38
+ git_add!('-u')
39
+ git_commit!(msg)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def versions
46
+ @versions ||= GitVersions.new
47
+ end
48
+
49
+ def check_requirements!
50
+ $logger.error "GIT: The working copy is not clean. Please commit your work before running Babelyoda tasks." unless clean?
51
+ end
52
+
53
+ def git_modified?(filename)
54
+ git_status.has_key?(filename)
55
+ end
56
+
57
+ def git_status
58
+ result = {}
59
+ `git status --porcelain`.scan(/^(\sM|\?\?)\s+(.*)$/).each do |m|
60
+ result[m[1]] = m[0]
61
+ end
62
+ result
63
+ end
64
+
65
+ def git_add!(filename)
66
+ ncmd = ['git', 'add', filename]
67
+ rc = Kernel.system(*ncmd)
68
+ $logger.error "GIT ERROR: #{ncmd}" unless rc
69
+ end
70
+
71
+ def git_commit!(msg)
72
+ ncmd = ['git', 'commit', '-m', msg]
73
+ rc = Kernel.system(*ncmd)
74
+ $logger.error "GIT ERROR: #{ncmd}" unless rc
75
+ end
76
+
77
+ def git_show(sha1, filename = nil)
78
+ ncmd = ['git', 'show', sha1]
79
+ IO.popen(ncmd) { |io|
80
+ blob = io.read
81
+ if filename
82
+ File.open(filename, 'w') {|f| f.write(blob) }
83
+ end
84
+ blob
85
+ }
86
+ $logger.error "GIT ERROR: #{ncmd}" unless $? == 0
87
+ end
88
+
89
+ def git_ls_sha1(filename)
90
+ matches = `git ls-files -s '#{filename}'`.match(/^\d{6}\s+([^\s]+)\s+.*$/)
91
+ $logger.error "GIT ERROR: Couldn't get SHA1 for: #{filename}" unless matches
92
+ matches[1]
93
+ end
94
+
95
+ def clean?
96
+ `git status 2>&1`.match(/^nothing to commit \(working directory clean\)$/)
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,39 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ module Babelyoda
5
+ class GitVersions
6
+
7
+ def initialize
8
+ @versions = load || {}
9
+ end
10
+
11
+ def exist?(filename)
12
+ @versions.has_key?(filename)
13
+ end
14
+
15
+ def filename
16
+ '.babelyoda/git_versions.yml'
17
+ end
18
+
19
+ def save!
20
+ FileUtils.mkdir_p(File.dirname(filename))
21
+ File.open(filename, 'w') {|f| f.write(@versions.to_yaml) }
22
+ end
23
+
24
+ def [](filename)
25
+ @versions[filename]
26
+ end
27
+
28
+ def []=(filename, value)
29
+ @versions[filename] = value
30
+ end
31
+
32
+ private
33
+
34
+ def load
35
+ @versions = YAML::load_file(filename) if File.exist?(filename)
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ require_relative 'logger'
2
+ require_relative 'strings'
3
+
4
+ module Babelyoda
5
+ class Ibtool
6
+ def self.extract_strings(xib_filename, language)
7
+ Dir.mktmpdir do |dir|
8
+ basename = File.basename(xib_filename, '.xib')
9
+ strings_filename = File.join(dir, "#{basename}.strings")
10
+ cmd = "ibtool --generate-strings-file '#{strings_filename}' '#{xib_filename}'"
11
+ $logger.error "IBTOOL ERROR: #{cmd}" unless Kernel.system(cmd)
12
+ return Babelyoda::Strings.new(strings_filename, language).read!
13
+ end
14
+ end
15
+
16
+ def self.localize(source_xib_fn, target_xib_fn, strings_fn)
17
+ # ibtool
18
+ # --strings-file path_to_strings/fr/MainWindow.strings # The latest localized strings for the French XIB
19
+ # --write path_to_project/fr.lproj/MainWindow.xib # The new French XIB that will be created
20
+ # path_to_project/English.lproj/MainWindow.new.xib # The new English XIB
21
+
22
+ ncmd = ['ibtool', '--strings-file', strings_fn, '--write', target_xib_fn, source_xib_fn]
23
+ rc = Kernel.system(*ncmd)
24
+ $logger.error "IBTOOL ERROR: #{ncmd}" unless rc
25
+ end
26
+
27
+ def self.localize_incrementally(source_xib_fn, target_xib_fn, strings_fn, old_source_xib_fn, old_target_xib_fn)
28
+ # ibtool
29
+ # --previous-file path_to_project/English.lproj/MainWindow.old.xib # The old English XIB
30
+ # --incremental-file path_to_project/fr.lproj/MainWindow.old.xib # The old French XIB
31
+ # --strings-file path_to_strings/fr/MainWindow.strings # The latest localized strings for the French XIB
32
+ # --localize-incremental
33
+ # --write path_to_project/fr.lproj/MainWindow.xib # The new French XIB that will be created
34
+ # path_to_project/English.lproj/MainWindow.new.xib # The new English XIB
35
+
36
+ ncmd = ['ibtool', '--previous-file', old_source_xib_fn, '--incremental-file', old_target_xib_fn,
37
+ '--strings-file', strings_fn, '--localize-incremental', '--write', target_xib_fn, source_xib_fn]
38
+ rc = Kernel.system(*ncmd)
39
+ $logger.error "IBTOOL ERROR: #{ncmd}" unless rc
40
+ end
41
+
42
+ def self.import_strings(filename, strings_filename)
43
+ ncmd = ['ibtool', '--import-strings-file', strings_filename, filename]
44
+ rc = Kernel.system(*ncmd)
45
+ $logger.error "IBTOOL ERROR: #{ncmd}" unless rc
46
+ end
47
+
48
+ private
49
+
50
+ end
51
+ end
@@ -0,0 +1,58 @@
1
+ module Babelyoda
2
+ class Keyset
3
+ attr_accessor :name
4
+ attr_accessor :keys
5
+
6
+ def self.keyset_name(filename)
7
+ raise ArgumentError.new("Invlaid filename for a .strings file: #{filename}") unless filename.match(/\.strings$/)
8
+ parts = File.join(File.dirname(filename), File.basename(filename, '.strings')).split('/')
9
+ parts.delete_if { |part| part.match(/\.lproj$/) }
10
+ File.join(parts)
11
+ end
12
+
13
+ def initialize(name)
14
+ @name = name
15
+ @keys = {}
16
+ end
17
+
18
+ def to_s ; "<#{self.class}: name = #{name}, keys.size = #{keys.size}>" ; end
19
+
20
+ def empty? ; keys.size == 0 ; end
21
+
22
+ def merge!(keyset, options = {})
23
+ result = { :new => 0, :updated => 0 }
24
+ keyset.keys.each_pair do |id, key|
25
+ if @keys.has_key?(id)
26
+ result[:updated] += 1 if @keys[id].merge!(key, options)
27
+ else
28
+ @keys[id] = key.dup
29
+ result[:new] += 1
30
+ end
31
+ end
32
+ return result
33
+ end
34
+
35
+ def merge_key!(localization_key)
36
+ if @keys.has_key?(localization_key.id)
37
+ @keys[localization_key.id].merge!(localization_key)
38
+ else
39
+ @keys[localization_key.id] = localization_key
40
+ end
41
+ end
42
+
43
+ def ensure_languages!(languages = [])
44
+ @keys.each_value do |key|
45
+ languages.each do |language|
46
+ key.values[language] ||= Babelyoda::LocalizationValue.new(language, '')
47
+ end
48
+ end
49
+ end
50
+
51
+ def drop_empty!
52
+ @keys.delete_if do |id, key|
53
+ key.drop_empty!
54
+ key.empty?
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,56 @@
1
+ module Babelyoda
2
+ class LocalizationKey
3
+ attr_reader :id
4
+ attr_reader :context
5
+ attr_reader :values
6
+
7
+ def initialize(id, context)
8
+ @id = id
9
+ @context = context
10
+ @values = {}
11
+ end
12
+
13
+ def <<(localization_value)
14
+ @values[localization_value.language.to_sym] = localization_value.dup
15
+ self
16
+ end
17
+
18
+ def merge!(localization_key, options = {})
19
+ updated = false
20
+
21
+ context_changed = false
22
+ if @context != localization_key.context
23
+ @context = localization_key.context
24
+ updated = context_changed = true
25
+ end
26
+
27
+ localization_key.values.each_value do |value|
28
+ if @values.has_key?(value.language.to_sym)
29
+ updated = true if @values[value.language.to_sym].merge!(value, options)
30
+ else
31
+ @values[value.language.to_sym] = value.dup
32
+ updated = true
33
+ end
34
+ end
35
+
36
+ # Mark all values as requiring translation if the context has changed.
37
+ if context_changed
38
+ @values.each_value do |value|
39
+ value.status = :requires_translation
40
+ end
41
+ end
42
+
43
+ return updated
44
+ end
45
+
46
+ def drop_empty!
47
+ @values.delete_if do |id, value|
48
+ value.text.empty?
49
+ end
50
+ end
51
+
52
+ def empty?
53
+ @values.empty?
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,26 @@
1
+ module Babelyoda
2
+ class LocalizationValue
3
+ attr_accessor :language
4
+ attr_accessor :status
5
+ attr_accessor :text
6
+
7
+ def initialize(language, text, status = :requires_translation)
8
+ @language, @text, @status = language.to_sym, text, status.to_sym
9
+ end
10
+
11
+ def merge!(other_value, options = {})
12
+ updated = false
13
+ options = { preserve: false }.merge!(options)
14
+ unless @language.to_sym == other_value.language.to_sym
15
+ raise RuntimeError.new("Can't merge values in different languages: #{@language.to_sym} and #{other_value.language.to_sym}")
16
+ end
17
+ if (!options[:preserve] || @status.to_sym == :requires_translation)
18
+ unless @text == other_value.text
19
+ @text = other_value.text
20
+ updated = true
21
+ end
22
+ end
23
+ return updated
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ require 'term/ansicolor'
2
+
3
+ module Babelyoda
4
+ class Logger
5
+ include Term::ANSIColor
6
+
7
+ def exe(cmd) ; putcmd cmd ; system cmd ; end
8
+ def putcmd(cmd) ; print magenta, "CMD: #{cmd}", reset, "\n" ; end
9
+ def status(msg) ; print blue, "--- #{msg} ---", reset, "\n" ; end
10
+ def success(msg) ; print green, bold, 'SUCCESS: ', msg, reset, "\n" ; end
11
+ def error(msg) ; print red, bold, 'ERROR: ', msg, reset, "\n" ; exit 1 ; end
12
+ def escape_cmd_args(args) ; args.collect{ |arg| "'#{arg}'"}.join(' ') ; end
13
+ end
14
+ end
15
+
16
+ $logger ||= Babelyoda::Logger.new
@@ -0,0 +1,10 @@
1
+ require_relative 'specification'
2
+
3
+ module Babelyoda
4
+ module Rake
5
+ def self.spec(&block)
6
+ spec = Babelyoda::Specification.load
7
+ block.call(spec) if spec
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,40 @@
1
+ require 'erb'
2
+
3
+ require_relative 'specification_loader'
4
+
5
+ module Babelyoda
6
+ class Specification
7
+ include Babelyoda::SpecificationLoader
8
+
9
+ attr_accessor :name
10
+ attr_accessor :development_language
11
+ attr_accessor :localization_languages
12
+ attr_accessor :engine
13
+ attr_accessor :source_files
14
+ attr_accessor :resources_folder
15
+ attr_accessor :xib_files
16
+ attr_accessor :strings_files
17
+ attr_accessor :scm
18
+
19
+ FILENAME = 'Babelfile'
20
+
21
+ def self.generate_default_babelfile
22
+ template_file_name = File.join(BABELYODA_PATH, 'templates', 'Babelfile.erb')
23
+ template = File.read(template_file_name)
24
+ File.open(FILENAME, "w+") do |f|
25
+ f.write(ERB.new(template).result())
26
+ end
27
+ end
28
+
29
+ def self.load
30
+ trace_spec = @spec.nil? && ::Rake.application.options.trace
31
+ @spec ||= load_from_file(filename = FILENAME)
32
+ @spec.dump if trace_spec && @spec
33
+ return @spec
34
+ end
35
+
36
+ def all_languages
37
+ [ development_language, localization_languages].flatten!
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,35 @@
1
+ require 'awesome_print'
2
+
3
+ module Babelyoda
4
+ module SpecificationLoader
5
+ def self.included(klass)
6
+ klass.extend ClassMethods
7
+ end
8
+
9
+ def initialize(*args)
10
+ super
11
+ yield(self) if block_given?
12
+ end
13
+
14
+ def method_missing(method_name, *args, &block)
15
+ msg = "You tried to call the method #{method_name}. There is no such method."
16
+ raise msg
17
+ end
18
+
19
+ def dump
20
+ ap self, :indent => -2
21
+ end
22
+
23
+ module ClassMethods
24
+
25
+ def load_from_file(filename)
26
+ return nil unless File.exist?(filename)
27
+ spec = eval(File.read(filename))
28
+ raise "Wrong specification class: #{spec.class.to_s}" unless spec.instance_of?(self)
29
+ return spec
30
+ end
31
+
32
+ end
33
+
34
+ end
35
+ end
@@ -1,11 +1,67 @@
1
- module BabelYoda
2
- class StringsHelper
3
- def self.safe_init_strings_file(path)
4
- unless File.exists? path
5
- empty_strings_file = File.join File.dirname(__FILE__), '..', '..', 'data', 'empty.strings'
6
- FileUtils.mkdir_p File.split(path)[0], :verbose => true
7
- FileUtils.cp empty_strings_file, path, :verbose => true
1
+ require 'rchardet19'
2
+
3
+ require_relative 'keyset'
4
+ require_relative 'strings_lexer'
5
+ require_relative 'strings_parser'
6
+
7
+ module Babelyoda
8
+ class Strings < Keyset
9
+ attr_reader :filename
10
+ attr_reader :language
11
+
12
+ def initialize(filename, language)
13
+ super(Babelyoda::Keyset.keyset_name(filename))
14
+ @filename, @language = filename, language
15
+ end
16
+
17
+ def read!
18
+ raise ArgumentError.new("File not found: #{filename}") unless File.exist?(@filename)
19
+ read
20
+ end
21
+
22
+ def read
23
+ if File.exist?(@filename)
24
+ File.open(@filename, read_mode) do |f|
25
+ lexer = StringsLexer.new
26
+ parser = StringsParser.new(lexer, @language)
27
+ parser.parse(f.read) do |localization_key|
28
+ merge_key!(localization_key)
29
+ end
30
+ end
31
+ end
32
+ self
33
+ end
34
+
35
+ def save!
36
+ FileUtils.mkdir_p(File.dirname(filename))
37
+ File.open(filename, "wb") do |f|
38
+ keys.each_pair do |id, key|
39
+ next unless key.values[language]
40
+ f << "/* #{key.context} */\n" if key.context
41
+ f << "\"#{id}\" = \"#{key.values[language].text}\";\n"
42
+ f << "\n"
43
+ end
44
+ end
45
+ end
46
+
47
+ def self.save_keyset(keyset, filename, language)
48
+ strings = self.new(filename, language)
49
+ strings.merge!(keyset)
50
+ strings.save!
51
+ end
52
+
53
+ private
54
+
55
+ def read_mode
56
+ cd = CharDet.detect(File.read(@filename))
57
+ encoding_str = Encoding.aliases[cd.encoding] || cd.encoding
58
+ encoding_str = 'UTF-8' if encoding_str == 'utf-8'
59
+ if (encoding_str != "UTF-8")
60
+ "rb:#{encoding_str}:UTF-8"
61
+ else
62
+ "r"
8
63
  end
9
64
  end
10
- end
65
+
66
+ end
11
67
  end
@@ -0,0 +1,12 @@
1
+ module Babelyoda
2
+ class StringsLexer
3
+ TOKENS = [ :multiline_comment, :singleline_comment, :string, :reserved0, :equal_sign, :semicolon ].freeze
4
+
5
+ def lex(str)
6
+ str.scan(/(\/\*.*\*\/)|(\s*\/\/.*\n)|((["])(?:\\?+.)*?\4)|(\s*=\s*)|(;)/).each do |m|
7
+ idx = m.index { |x| x }
8
+ yield TOKENS[idx], m[idx].strip
9
+ end
10
+ end
11
+ end
12
+ end