babelyoda 1.6.0 → 2.0.0

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 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