i18n-translators-tools 0.1
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/README.md +229 -0
- data/Rakefile +18 -0
- data/bin/i18n-translate +304 -0
- data/i18n-translators-tools.gemspec +27 -0
- data/lib/i18n-translate.rb +13 -0
- data/lib/i18n/backend/translate.rb +38 -0
- data/lib/i18n/processor.rb +93 -0
- data/lib/i18n/processor/gettext.rb +127 -0
- data/lib/i18n/processor/ruby.rb +41 -0
- data/lib/i18n/processor/yaml.rb +24 -0
- data/lib/i18n/translate.rb +413 -0
- data/test/locale/src/cze.po +32 -0
- data/test/locale/src/cze.rb +32 -0
- data/test/locale/src/cze.yml +22 -0
- data/test/locale/src/default.yml +14 -0
- data/test/tc_translate.rb +166 -0
- metadata +81 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# vi: fenc=utf-8:expandtab:ts=2:sw=2:sts=2
|
3
|
+
#
|
4
|
+
# @author: Petr Kovar <pejuko@gmail.com>
|
5
|
+
|
6
|
+
require 'rubygems'
|
7
|
+
require 'find'
|
8
|
+
|
9
|
+
spec = Gem::Specification.new do |s|
|
10
|
+
s.platform = Gem::Platform::RUBY
|
11
|
+
s.summary = "I18n transation utility which helps to manage files with locales."
|
12
|
+
s.email = "pejuko@gmail.com"
|
13
|
+
s.authors = ["Petr Kovar"]
|
14
|
+
s.name = 'i18n-translators-tools'
|
15
|
+
s.version = '0.1'
|
16
|
+
s.date = '2010-07-16'
|
17
|
+
s.requirements << 'i18n' << 'ya2yaml'
|
18
|
+
s.require_path = 'lib'
|
19
|
+
s.files = ["bin/i18n-translate", "test/tc_translate.rb", "test/locale/src/default.yml", "test/locale/src/cze.yml", "test/locale/src/cze.rb", "test/locale/src/cze.po", "lib/i18n-translate.rb", "lib/i18n/translate.rb", "lib/i18n/processor.rb", "lib/i18n/processor/yaml.rb", "lib/i18n/processor/ruby.rb", "lib/i18n/processor/gettext.rb", "lib/i18n/backend/translate.rb", "README.md", "i18n-translators-tools.gemspec", "Rakefile"]
|
20
|
+
s.executables = ["i18n-translate"]
|
21
|
+
s.description = <<EOF
|
22
|
+
This package brings you useful utility which can help you to handle locale files
|
23
|
+
and translations in your Ruby projects. Read README.md file and run i18n-translate
|
24
|
+
without parameters for more information.
|
25
|
+
EOF
|
26
|
+
end
|
27
|
+
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# vi: fenc=utf-8:expandtab:ts=2:sw=2:sts=2
|
3
|
+
#
|
4
|
+
# @author: Petr Kovar <pejuko@gmail.com>
|
5
|
+
|
6
|
+
require 'i18n'
|
7
|
+
|
8
|
+
dir = File.expand_path(File.dirname(__FILE__))
|
9
|
+
$:.unshift(dir) unless $:.include?(dir)
|
10
|
+
|
11
|
+
require 'i18n/backend/translate'
|
12
|
+
require 'i18n/translate'
|
13
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# vi: fenc=utf-8:expandtab:ts=2:sw=2:sts=2
|
3
|
+
#
|
4
|
+
# @author: Petr Kovar <pejuko@gmail.com>
|
5
|
+
|
6
|
+
module I18n
|
7
|
+
|
8
|
+
module Backend
|
9
|
+
# It is highly recommended to use Translator wit Fallback plugin
|
10
|
+
#
|
11
|
+
# I18n::Backend::Simple.send(:include, I18n::Backend::Translator)
|
12
|
+
# I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
|
13
|
+
#
|
14
|
+
# notice that Translator have to be included BEFORE Fallback otherwise
|
15
|
+
# the fallback will get Hash (even with empty translation) and won't work.
|
16
|
+
module Translate
|
17
|
+
|
18
|
+
# wrapper which can work with both format
|
19
|
+
# the simple and the Translator's
|
20
|
+
def translate(locale, key, options = {})
|
21
|
+
result = super(locale, key, options)
|
22
|
+
return result unless result.kind_of?(Hash)
|
23
|
+
return nil unless result[:t]
|
24
|
+
|
25
|
+
tr = result[:t]
|
26
|
+
values = options.except(*I18n::Backend::Base::RESERVED_KEYS)
|
27
|
+
|
28
|
+
tr = resolve(locale, key, tr, options)
|
29
|
+
tr = interpolate(locale, tr, values) if values
|
30
|
+
|
31
|
+
tr
|
32
|
+
end
|
33
|
+
|
34
|
+
end # module Backend::Translator
|
35
|
+
end # module Backend
|
36
|
+
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# vi: fenc=utf-8:expandtab:ts=2:sw=2:sts=2
|
3
|
+
#
|
4
|
+
# @author: Petr Kovar <pejuko@gmail.com>
|
5
|
+
|
6
|
+
module I18n::Translate
|
7
|
+
|
8
|
+
module Processor
|
9
|
+
@processors = []
|
10
|
+
|
11
|
+
class << self
|
12
|
+
attr_reader :processors
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.<<(processor)
|
16
|
+
@processors << processor
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.read(fname, tr)
|
20
|
+
processor = find_processor(fname)
|
21
|
+
raise "Unknown file format" unless processor
|
22
|
+
worker = processor.new(fname, tr)
|
23
|
+
worker.read
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.write(fname, data, tr)
|
27
|
+
processor = find_processor(fname)
|
28
|
+
raise "Unknown file format `#{fname}'" unless processor
|
29
|
+
worker = processor.new(fname, tr)
|
30
|
+
worker.write(data)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.find_processor(fname)
|
34
|
+
@processors.each do |processor|
|
35
|
+
return processor if processor.can_handle?(fname)
|
36
|
+
end
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
class Template
|
42
|
+
FORMAT = []
|
43
|
+
|
44
|
+
def self.inherited(processor)
|
45
|
+
Processor << processor
|
46
|
+
end
|
47
|
+
|
48
|
+
attr_reader :filename, :translate
|
49
|
+
|
50
|
+
def initialize(fname, tr)
|
51
|
+
@filename = fname
|
52
|
+
@translate = tr
|
53
|
+
end
|
54
|
+
|
55
|
+
def read
|
56
|
+
data = File.open(@filename, mode("r")){ |f| f.read }
|
57
|
+
import(data)
|
58
|
+
end
|
59
|
+
|
60
|
+
def write(data)
|
61
|
+
File.open(@filename, mode("w")) {|f| f << export(data)}
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.can_handle?(fname)
|
65
|
+
fname =~ %r{\.([^\.]+)$}
|
66
|
+
self::FORMAT.include?($1)
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
def import(data)
|
72
|
+
data
|
73
|
+
end
|
74
|
+
|
75
|
+
def export(data)
|
76
|
+
data
|
77
|
+
end
|
78
|
+
|
79
|
+
def mode(m)
|
80
|
+
mode = m.dup
|
81
|
+
mode << ":" << @translate.options[:encoding] if defined?(Encoding)
|
82
|
+
mode
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
require 'i18n/processor/yaml'
|
91
|
+
require 'i18n/processor/ruby'
|
92
|
+
require 'i18n/processor/gettext'
|
93
|
+
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# vi: fenc=utf-8:expandtab:ts=2:sw=2:sts=2
|
3
|
+
#
|
4
|
+
# @author: Petr Kovar <pejuko@gmail.com>
|
5
|
+
|
6
|
+
module I18n::Translate::Processor
|
7
|
+
|
8
|
+
class Gettext < Template
|
9
|
+
FORMAT = ['po']
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def import(data)
|
14
|
+
hash = {}
|
15
|
+
|
16
|
+
entry = {}
|
17
|
+
key = nil
|
18
|
+
last = nil
|
19
|
+
data.each_line do |line|
|
20
|
+
# empty line starts new entry
|
21
|
+
if line =~ %r{^\s*$}
|
22
|
+
if not entry.empty? and key
|
23
|
+
I18n::Translate.set(key, entry, hash, @translate.options[:separator])
|
24
|
+
end
|
25
|
+
key = nil
|
26
|
+
last = nil
|
27
|
+
entry = {}
|
28
|
+
next
|
29
|
+
end
|
30
|
+
|
31
|
+
case line
|
32
|
+
|
33
|
+
# translator's comment
|
34
|
+
when %r{^# (.*)$}
|
35
|
+
entry["comment"] = $1.to_s.strip
|
36
|
+
|
37
|
+
# translator's comment
|
38
|
+
when %r{^#: (.*)$}
|
39
|
+
entry["line"] = $1.to_s.strip
|
40
|
+
|
41
|
+
# flag
|
42
|
+
when %r{^#, (.*)$}
|
43
|
+
flags = $1.split(",").compact.map{|x| x.strip}
|
44
|
+
fuzzy = flags.delete("fuzzy")
|
45
|
+
unless fuzzy
|
46
|
+
entry["flag"] = "ok"
|
47
|
+
else
|
48
|
+
flags.delete_if{|x| not I18n::Translate::FLAGS.include?(x)}
|
49
|
+
entry["flag"] = flags.first unless flags.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
# old default
|
53
|
+
when %r{^#\| msgid (.*)$}
|
54
|
+
entry["old"] = $1.to_s.strip
|
55
|
+
|
56
|
+
# key
|
57
|
+
when %r{^msgctxt "(.*)"$}
|
58
|
+
key = $1.to_s.strip
|
59
|
+
last = "key"
|
60
|
+
|
61
|
+
# default
|
62
|
+
when %r{^msgid "(.*)"$}
|
63
|
+
last = "default"
|
64
|
+
entry[last] = $1.to_s.strip
|
65
|
+
|
66
|
+
# translation
|
67
|
+
when %r{^msgstr "(.*)"$}
|
68
|
+
last = "t"
|
69
|
+
entry[last] = $1.to_s.strip
|
70
|
+
|
71
|
+
# string continuation
|
72
|
+
when %r{^"(.*)"$}
|
73
|
+
if last == "key"
|
74
|
+
key = "#{key}#{$1.to_s.strip}"
|
75
|
+
elsif last
|
76
|
+
entry[last] = "#{entry[last]}#{$1.to_s.strip}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# last line at end of file
|
82
|
+
if not entry.empty? and key
|
83
|
+
I18n::Translate.set(key, entry, hash, @translate.options[:separator])
|
84
|
+
end
|
85
|
+
|
86
|
+
{@translate.lang => hash}
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
# this export ignores data
|
91
|
+
def export(data)
|
92
|
+
str = ""
|
93
|
+
keys = I18n::Translate.hash_to_keys(@translate.default).sort
|
94
|
+
|
95
|
+
keys.each do |key|
|
96
|
+
entry = [""]
|
97
|
+
value = @translate.find(key, @translate.target)
|
98
|
+
next unless value
|
99
|
+
|
100
|
+
if value.kind_of?(String)
|
101
|
+
entry << %~msgctxt #{key.inspect}~
|
102
|
+
entry << %~msgid #{@translate.find(key, @translate.default).to_s.inspect}~
|
103
|
+
entry << %~msgstr #{value.to_s.inspect}~
|
104
|
+
else
|
105
|
+
entry << %~# #{value["comment"]}~ unless value["comment"].to_s.empty?
|
106
|
+
entry << %~#: #{value["line"]}~ unless value["line"].to_s.empty?
|
107
|
+
flags = []
|
108
|
+
flags << "fuzzy" if value["fuzzy"]
|
109
|
+
flags << value["flag"] unless value["flag"].to_s.strip.empty?
|
110
|
+
entry << %~#, #{flags.join(", ")}~ unless flags.empty?
|
111
|
+
entry << %~#| msgid #{value["old"]}~ unless value["old"].to_s.empty?
|
112
|
+
entry << %~msgctxt #{key.inspect}~
|
113
|
+
entry << %~msgid #{value["default"].to_s.inspect}~
|
114
|
+
entry << %~msgstr #{value["t"].to_s.inspect}~
|
115
|
+
end
|
116
|
+
|
117
|
+
entry << ""
|
118
|
+
|
119
|
+
str << entry.join("\n")
|
120
|
+
end
|
121
|
+
|
122
|
+
str
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# vi: fenc=utf-8:expandtab:ts=2:sw=2:sts=2
|
3
|
+
#
|
4
|
+
# @author: Petr Kovar <pejuko@gmail.com>
|
5
|
+
|
6
|
+
module I18n::Translate::Processor
|
7
|
+
|
8
|
+
class Ruby < Template
|
9
|
+
FORMAT = ['rb']
|
10
|
+
|
11
|
+
protected
|
12
|
+
|
13
|
+
def import(data)
|
14
|
+
eval(data)
|
15
|
+
end
|
16
|
+
|
17
|
+
# serialize hash to string
|
18
|
+
def export(data, indent=0)
|
19
|
+
str = "{\n"
|
20
|
+
|
21
|
+
data.keys.sort{|k1,k2| k1.to_s <=> k2.to_s}.each_with_index do |k, i|
|
22
|
+
str << (" " * (indent+1))
|
23
|
+
str << "#{k.inspect} => "
|
24
|
+
if data[k].kind_of?(Hash)
|
25
|
+
str << export(data[k], indent+1)
|
26
|
+
else
|
27
|
+
str << data[k].inspect
|
28
|
+
end
|
29
|
+
str << "," if i < (data.keys.size - 1)
|
30
|
+
str << "\n"
|
31
|
+
end
|
32
|
+
|
33
|
+
str << (" " * (indent))
|
34
|
+
str << "}"
|
35
|
+
|
36
|
+
str
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# vi: fenc=utf-8:expandtab:ts=2:sw=2:sts=2
|
3
|
+
#
|
4
|
+
# @author: Petr Kovar <pejuko@gmail.com>
|
5
|
+
|
6
|
+
require 'yaml'
|
7
|
+
require 'ya2yaml'
|
8
|
+
|
9
|
+
module I18n::Translate::Processor
|
10
|
+
class YAML < Template
|
11
|
+
FORMAT = ['yml', 'yaml']
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
def import(data)
|
16
|
+
::YAML.load(data)
|
17
|
+
end
|
18
|
+
|
19
|
+
def export(data)
|
20
|
+
data.ya2yaml
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,413 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
# vi: fenc=utf-8:expandtab:ts=2:sw=2:sts=2
|
3
|
+
#
|
4
|
+
# @author: Petr Kovar <pejuko@gmail.com>
|
5
|
+
|
6
|
+
require 'fileutils'
|
7
|
+
require 'find'
|
8
|
+
|
9
|
+
require 'i18n/processor'
|
10
|
+
|
11
|
+
# I18n::Translate introduces new format for translations. To make
|
12
|
+
# I18n.t work properly you need to include Translator's backend:
|
13
|
+
#
|
14
|
+
# I18n::Backend::Simple.send(:include, I18n::Backend::Translate)
|
15
|
+
# I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
|
16
|
+
#
|
17
|
+
# notice that Translator have to be included BEFORE Fallback otherwise
|
18
|
+
# the fallback will get Hash (even with empty translation) and won't work.
|
19
|
+
#
|
20
|
+
# It is hightly recommended to use Fallback backend together with Translate.
|
21
|
+
# If you have experienced nil or empty translations this can fix the problem.
|
22
|
+
#
|
23
|
+
# Format of entry:
|
24
|
+
#
|
25
|
+
# old: old default string
|
26
|
+
# default: new default string
|
27
|
+
# comment: translator's comments
|
28
|
+
# t: translation
|
29
|
+
# line: the lines, where is this key used # not implemented yet
|
30
|
+
# flag: ok || incomplete || changed || untranslated
|
31
|
+
# fuzzy: true # exists only where flag != ok (nice to have when you want
|
32
|
+
# edit files manualy)
|
33
|
+
#
|
34
|
+
# This format is for leaves in the tree hiearchy for plurals it should look like
|
35
|
+
#
|
36
|
+
# key:
|
37
|
+
# one:
|
38
|
+
# old:
|
39
|
+
# default:
|
40
|
+
# t:
|
41
|
+
# ...
|
42
|
+
# other:
|
43
|
+
# old:
|
44
|
+
# default:
|
45
|
+
# t:
|
46
|
+
# ...
|
47
|
+
#
|
48
|
+
module I18n::Translate
|
49
|
+
|
50
|
+
FLAGS = %w(ok incomplete changed untranslated)
|
51
|
+
FORMATS = %w(yml rb po) # the first one is preferred if :format => auto
|
52
|
+
|
53
|
+
# returns flat array of all keys e.g. ["system.message.ok", "system.message.error", ...]
|
54
|
+
def self.hash_to_keys(hash, separator=".", prefix="")
|
55
|
+
res = []
|
56
|
+
hash.keys.each do |key|
|
57
|
+
str = prefix.empty? ? key : "#{prefix}#{separator}#{key}"
|
58
|
+
if hash[key].kind_of?(Hash)
|
59
|
+
str = hash_to_keys( hash[key], separator, str )
|
60
|
+
end
|
61
|
+
res << str
|
62
|
+
end
|
63
|
+
res.flatten
|
64
|
+
end
|
65
|
+
|
66
|
+
# returns what is stored under key
|
67
|
+
def self.find(key, hash, separator=".")
|
68
|
+
h = hash
|
69
|
+
path = key.to_s.split(separator)
|
70
|
+
path.each do |key|
|
71
|
+
h = h[key]
|
72
|
+
return nil unless h
|
73
|
+
end
|
74
|
+
h
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.set(key, value, hash, separator=".")
|
78
|
+
h = hash
|
79
|
+
path = key.to_s.split(separator)
|
80
|
+
path[0..-2].each do |chunk|
|
81
|
+
h[chunk] ||= {}
|
82
|
+
h = h[chunk]
|
83
|
+
end
|
84
|
+
unless value
|
85
|
+
h[path[-1]] = nil
|
86
|
+
else
|
87
|
+
h[path[-1]] = value
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# scans :locale_dir for files with valid formats and returns
|
92
|
+
# list of files with locales. If block is given then
|
93
|
+
# it creates Translate object for each entry and pass it to the block
|
94
|
+
def self.scan(opts=Translate::DEFAULT_OPTIONS, &block)
|
95
|
+
o = Translate::DEFAULT_OPTIONS.merge(opts)
|
96
|
+
o[:exclude] ||= []
|
97
|
+
|
98
|
+
entries = []
|
99
|
+
if o[:deep] == true
|
100
|
+
Find.find(o[:locale_dir]) {|e| entries << e}
|
101
|
+
else
|
102
|
+
entries = Dir[File.join(o[:locale_dir], "*")]
|
103
|
+
end
|
104
|
+
|
105
|
+
locales = []
|
106
|
+
entries.each do |entry|
|
107
|
+
locale, format = Translate.valid_file?(entry, o[:format])
|
108
|
+
if (not format) or (locale == o[:default])
|
109
|
+
puts "#{entry}...skipping" if o[:verbose]
|
110
|
+
next unless format
|
111
|
+
next if locale == o[:default]
|
112
|
+
end
|
113
|
+
|
114
|
+
exclude = false
|
115
|
+
o[:exclude].each do |ex|
|
116
|
+
if entry =~ %r|#{ex}|
|
117
|
+
exclude = true
|
118
|
+
break
|
119
|
+
end
|
120
|
+
end
|
121
|
+
puts "#{entry}...excluded" if exclude and o[:verbose]
|
122
|
+
next if exclude
|
123
|
+
|
124
|
+
locales << entry
|
125
|
+
dir = File.dirname(entry)
|
126
|
+
|
127
|
+
if block
|
128
|
+
yield Translate.new(locale, o.merge({:format => format, :locale_dir => dir, :default => o[:default]}))
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
locales
|
134
|
+
end
|
135
|
+
|
136
|
+
|
137
|
+
|
138
|
+
# it breaks proc and lambdas objects
|
139
|
+
class Translate
|
140
|
+
DEFAULT_OPTIONS = {
|
141
|
+
:separator => ".", # default key separator e.g. "model.article.message.not.found"
|
142
|
+
:locale_dir => "locale", # where to search for files
|
143
|
+
:default => "default", # default name for file containing default app's key => string
|
144
|
+
:force_encoding => true, # in ruby 1.9 forces string encoding
|
145
|
+
:encoding => "utf-8", # encoding name to be forced to
|
146
|
+
:format => "auto" # auto, rb, yml
|
147
|
+
}
|
148
|
+
|
149
|
+
attr_reader :default, :target, :merge, :options, :lang, :default_file, :lang_file
|
150
|
+
|
151
|
+
# loads default and lang files
|
152
|
+
def initialize(lang, opts={})
|
153
|
+
@lang = lang.to_s
|
154
|
+
raise "Empty locale" if @lang.empty?
|
155
|
+
@options = DEFAULT_OPTIONS.merge(opts)
|
156
|
+
@options[:default_format] ||= @options[:format]
|
157
|
+
|
158
|
+
@default, @default_file = load_locale( @options[:default], @options[:default_format] )
|
159
|
+
@target, @lang_file = load_locale( @lang )
|
160
|
+
|
161
|
+
merge!
|
162
|
+
end
|
163
|
+
|
164
|
+
# check if the file has supported format
|
165
|
+
def self.valid_file?(fname, format=Translate::DEFAULT_OPTIONS[:format])
|
166
|
+
pattern = ".*?"
|
167
|
+
pattern = format if format != "auto"
|
168
|
+
fname =~ /\/?([^\/]*?)\.(#{pattern})$/
|
169
|
+
locale, format = $1, $2
|
170
|
+
if I18n::Translate::FORMATS.include?($2)
|
171
|
+
return [locale, format]
|
172
|
+
end
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
|
176
|
+
# will merge only one key and returns hash
|
177
|
+
# {
|
178
|
+
# :key => 'key',
|
179
|
+
# :default => '', # value set in default file
|
180
|
+
# :old_default => '', # value set as old in target file
|
181
|
+
# (value from default file from last translation
|
182
|
+
# if the field has changed)
|
183
|
+
# :old_t => '', # if flag == 'changed' then old_t = t and t = ''
|
184
|
+
# :t => '', # value set in target file
|
185
|
+
# :line => 'some/file.rb: 44', # name of source file and number of line
|
186
|
+
# (a copy in target file from default file)
|
187
|
+
# !!! line is unused for now
|
188
|
+
# :comment => '' # a comment added by a translator
|
189
|
+
# :flag => ok || incomplete || changed || untranslated # set by merging tool except incomplete
|
190
|
+
# which is set by translator
|
191
|
+
# }
|
192
|
+
def [](key)
|
193
|
+
d = I18n::Translate.find(key, @default, @options[:separator])
|
194
|
+
raise "Translate#[key]: wrong key '#{key}'" unless d
|
195
|
+
|
196
|
+
entry = {"key" => key, "default" => d}
|
197
|
+
|
198
|
+
# translation doesn't exist
|
199
|
+
trg = I18n::Translate.find(key, @target, @options[:separator])
|
200
|
+
if (not trg) or
|
201
|
+
(trg.kind_of?(String) and trg.strip.empty?) or
|
202
|
+
(trg.kind_of?(Hash) and trg["t"].to_s.strip.empty?)
|
203
|
+
entry["old_default"] = ""
|
204
|
+
entry["old_t"] = ""
|
205
|
+
entry["t"] = ""
|
206
|
+
entry["comment"] = trg.kind_of?(Hash) ? trg["comment"].to_s.strip : ""
|
207
|
+
entry["flag"] = "untranslated"
|
208
|
+
return entry
|
209
|
+
end
|
210
|
+
|
211
|
+
# default has changed => new translation is probably required
|
212
|
+
if trg.kind_of?(Hash)
|
213
|
+
entry["old_t"] = trg["t"].to_s.strip
|
214
|
+
entry["t"] = ""
|
215
|
+
entry["comment"] = trg["comment"].to_s.strip
|
216
|
+
entry["flag"] = "changed"
|
217
|
+
|
218
|
+
if d != trg["default"]
|
219
|
+
entry["old_default"] = trg["default"].to_s.strip
|
220
|
+
return entry
|
221
|
+
elsif not trg["old"].to_s.strip.empty?
|
222
|
+
entry["old_default"] = trg["old"].to_s.strip
|
223
|
+
return entry
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# nothing has changed
|
228
|
+
entry["old_default"] = trg.kind_of?(Hash) ? trg["old"].to_s.strip : ""
|
229
|
+
entry["old_t"] = ""
|
230
|
+
entry["t"] = trg.kind_of?(Hash) ? trg["t"].to_s.strip : trg.to_s.strip
|
231
|
+
entry["comment"] = trg.kind_of?(Hash) ? trg["comment"].to_s.strip : ""
|
232
|
+
entry["flag"] = (trg.kind_of?(Hash) and trg["flag"]) ? trg["flag"].to_s.strip : "ok"
|
233
|
+
|
234
|
+
entry
|
235
|
+
end
|
236
|
+
|
237
|
+
# wrapper for I18n::Translate.find with presets options
|
238
|
+
def find(key, hash=@translate, separator=@options[:separator])
|
239
|
+
I18n::Translate.find(key, hash, separator)
|
240
|
+
end
|
241
|
+
|
242
|
+
# will create path in @target for 'key' and set the 'value'
|
243
|
+
def []=(key, value)
|
244
|
+
I18n::Translate.set(key, value, @target, @options[:separator])
|
245
|
+
end
|
246
|
+
|
247
|
+
# merge merged and edited hash into @target
|
248
|
+
# translation can be hash or array
|
249
|
+
# * array format is the same as self.merge is
|
250
|
+
# [ {key => , t =>, ...}, {key =>, ...}, ... ]
|
251
|
+
# * hash format is supposed to be the format obtained from web form
|
252
|
+
# {:key => {t =>, ...}, :key => {...}, ...}
|
253
|
+
def assign(translation)
|
254
|
+
translation.each do |transl|
|
255
|
+
key, values = nil
|
256
|
+
if transl.kind_of?(Hash)
|
257
|
+
# merge format: [{key => , t =>, ...}, ...]
|
258
|
+
key, values = transl["key"], transl
|
259
|
+
elsif transl.kind_of?(Array)
|
260
|
+
# web format: {key => {t => }, ...}
|
261
|
+
key, values = transl
|
262
|
+
end
|
263
|
+
|
264
|
+
old_t = values["old_t"].to_s.strip
|
265
|
+
new_t = values["t"].to_s.strip
|
266
|
+
default = values["default"].to_s.strip
|
267
|
+
old_default = values["old_default"].to_s.strip
|
268
|
+
flag = values["flag"].to_s.strip
|
269
|
+
comment = values["comment"].to_s.strip
|
270
|
+
|
271
|
+
if old_t.respond_to?("force_encoding") and @options[:force_encoding]
|
272
|
+
enc = @options[:encoding]
|
273
|
+
old_t.force_encoding(enc)
|
274
|
+
new_t.force_encoding(enc)
|
275
|
+
default.force_encoding(enc)
|
276
|
+
old_default.force_encoding(enc)
|
277
|
+
flag.force_encoding(enc)
|
278
|
+
comment.force_encoding(enc)
|
279
|
+
end
|
280
|
+
|
281
|
+
trg = {
|
282
|
+
"comment" => comment,
|
283
|
+
"flag" => flag
|
284
|
+
}
|
285
|
+
|
286
|
+
if flag == "ok"
|
287
|
+
trg["t"] = new_t.empty? ? old_t : new_t
|
288
|
+
trg["default"] = default
|
289
|
+
trg["old"] = ""
|
290
|
+
else
|
291
|
+
trg["t"] = new_t.empty? ? old_t : new_t
|
292
|
+
trg["default"] = default
|
293
|
+
trg["old"] = old_default
|
294
|
+
end
|
295
|
+
|
296
|
+
# make fallback work
|
297
|
+
trg["t"] = nil if trg["t"].empty?
|
298
|
+
|
299
|
+
# say that this entry is not completed yet
|
300
|
+
# useful if you edit files in text editor and serching for next one
|
301
|
+
trg["fuzzy"] = true if flag != "ok"
|
302
|
+
|
303
|
+
self[key] = trg
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
# re-read @target data from the disk and create @merge
|
308
|
+
def reload!
|
309
|
+
@target, @lang_file = load_locale( @lang )
|
310
|
+
merge!
|
311
|
+
end
|
312
|
+
|
313
|
+
# merge @default and @target into list @merge
|
314
|
+
def merge!
|
315
|
+
@merge = merge_locale
|
316
|
+
end
|
317
|
+
|
318
|
+
# export @target to file
|
319
|
+
def export!
|
320
|
+
save_locale(@lang)
|
321
|
+
end
|
322
|
+
|
323
|
+
# throw away translators metadata and convert
|
324
|
+
# hash to default I18n format
|
325
|
+
def strip!
|
326
|
+
keys = I18n::Translate.hash_to_keys(@default, @options[:separator])
|
327
|
+
keys.each do |key|
|
328
|
+
entry = I18n::Translate.find(key, @target, @options[:separator])
|
329
|
+
raise "Translate#[key]: wrong key '#{key}'" unless entry
|
330
|
+
next unless entry.kind_of?(Hash)
|
331
|
+
self[key] = entry["t"]
|
332
|
+
end
|
333
|
+
|
334
|
+
self
|
335
|
+
end
|
336
|
+
|
337
|
+
# returns statistics hash
|
338
|
+
# {:total => N, :ok => N, :changed => N, :incomplete => N, :untranslated => N, :fuzzy => N, :progress => N}
|
339
|
+
def stat
|
340
|
+
stat = {
|
341
|
+
:total => @merge.size,
|
342
|
+
:ok => @merge.select{|e| e["flag"] == "ok"}.size,
|
343
|
+
:changed => @merge.select{|e| e["flag"] == "changed"}.size,
|
344
|
+
:incomplete => @merge.select{|e| e["flag"] == "incomplete"}.size,
|
345
|
+
:untranslated => @merge.select{|e| e["flag"] == "untranslated"}.size,
|
346
|
+
:fuzzy => @merge.select{|e| e["flag"] != "ok"}.size
|
347
|
+
}
|
348
|
+
stat[:progress] = (stat[:ok].to_f / stat[:total].to_f) * 100
|
349
|
+
stat
|
350
|
+
end
|
351
|
+
|
352
|
+
def to_yaml
|
353
|
+
trg = {@lang => @target}
|
354
|
+
#YAML.dump(trg)
|
355
|
+
trg.ya2yaml
|
356
|
+
end
|
357
|
+
|
358
|
+
def to_rb
|
359
|
+
trg = {@lang => @target}
|
360
|
+
trg.to_rb
|
361
|
+
end
|
362
|
+
|
363
|
+
protected
|
364
|
+
|
365
|
+
# returns first file for @lang.type
|
366
|
+
def file_name(lang, type=@options[:format])
|
367
|
+
fname = "#{@options[:locale_dir]}/#{lang}.#{type}"
|
368
|
+
if type == "auto"
|
369
|
+
pattern = "#{@options[:locale_dir]}/#{lang}.*"
|
370
|
+
fname = Dir[pattern].select{|x| Translate.valid_file?(x)}.first
|
371
|
+
end
|
372
|
+
fname = "#{@options[:locale_dir]}/#{lang}.#{FORMATS.first}" unless fname
|
373
|
+
fname
|
374
|
+
end
|
375
|
+
|
376
|
+
# loads locales from .rb or .yml file
|
377
|
+
def load_locale(lang, type=@options[:format])
|
378
|
+
fname = file_name(lang, type)
|
379
|
+
|
380
|
+
if File.exists?(fname)
|
381
|
+
return [Processor.read(fname, self)[lang], fname]
|
382
|
+
else
|
383
|
+
STDERR << "Warning: I18n::Translate#load_locale: file `#{fname}' does NOT exists. Creating empty locale.\n"
|
384
|
+
end
|
385
|
+
|
386
|
+
[{}, fname]
|
387
|
+
end
|
388
|
+
|
389
|
+
# save to the first file found as lang.*
|
390
|
+
# detects .rb and .yml
|
391
|
+
def save_locale(lang)
|
392
|
+
fname = file_name(lang)
|
393
|
+
backup(fname)
|
394
|
+
Processor.write(fname, {@lang => @target}, self)
|
395
|
+
end
|
396
|
+
|
397
|
+
# backup file if file exists
|
398
|
+
def backup(fname)
|
399
|
+
FileUtils.cp(fname, "#{fname}.bak") if File.exists?(fname)
|
400
|
+
end
|
401
|
+
|
402
|
+
# creates array of hashes as specified in self[] function
|
403
|
+
def merge_locale
|
404
|
+
keys = I18n::Translate.hash_to_keys(@default, @options[:separator])
|
405
|
+
keys.sort!
|
406
|
+
keys.inject([]) do |sum, key|
|
407
|
+
sum << self[key]
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
|
412
|
+
end # class Translate
|
413
|
+
end # module DML
|