grosser-pomo 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +43 -0
- data/VERSION.yml +4 -0
- data/lib/pomo.rb +3 -0
- data/lib/pomo/po_file.rb +117 -0
- data/lib/pomo/translation.rb +44 -0
- data/spec/pomo/po_file_spec.rb +110 -0
- data/spec/pomo/translation_spec.rb +119 -0
- data/spec/spec_helper.rb +10 -0
- metadata +63 -0
data/README.markdown
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
A simple and extendable .mo and .po file parser/generator.
|
2
|
+
--mo file parser and writer are missing atm--
|
3
|
+
|
4
|
+
Advanteges over [original po-parser](http://github.com/mutoh/gettext/blob/abf96713327cc4c5d35f0a772f3b75ff4819450c/lib/gettext/poparser.rb):
|
5
|
+
|
6
|
+
- simple architecture + easy to extend/modify
|
7
|
+
- emtpy msgstr translations are read
|
8
|
+
- comments are included
|
9
|
+
- fuzzy can be set/unset
|
10
|
+
- multiple translations can be combined in a new po file(with comments and fuzzy and ...)
|
11
|
+
- po files can be written from any kind of input
|
12
|
+
|
13
|
+
Setup
|
14
|
+
=====
|
15
|
+
sudo gem install grosser-pomo -s http://gems.github.com/
|
16
|
+
|
17
|
+
###Static interface
|
18
|
+
#parse po files
|
19
|
+
translations = Pomo::PoFile.parse(File.read('xxx.po')) + Pomo::PoFile.parse(File.read('yyy.po'))
|
20
|
+
|
21
|
+
#and use the data...
|
22
|
+
msgids = translations.reject{|t|t.plural? or t.fuzzy?}.map(&:msgid)
|
23
|
+
|
24
|
+
#or write a new po file (unique by msgid)...
|
25
|
+
File.open('xxx.po','w){|f|f.print(Pomo::PoFile.to_text(translations))}
|
26
|
+
|
27
|
+
###Instance interface
|
28
|
+
p = PoMo::PoFile.new
|
29
|
+
p.add_translations_from_text(File.read('...'))
|
30
|
+
...
|
31
|
+
p.translations
|
32
|
+
p.to_text
|
33
|
+
|
34
|
+
TODO
|
35
|
+
====
|
36
|
+
- extracting of version/pluralisation_rule/plurals/translator... (from msgid "")
|
37
|
+
- mo writing/reading (this is the hardest part imo...)
|
38
|
+
|
39
|
+
Author
|
40
|
+
======
|
41
|
+
[Michael Grosser](http://pragmatig.wordpress.com)
|
42
|
+
grosser.michael@gmail.com
|
43
|
+
Hereby placed under public domain, do what you want, just do not hold me accountable...
|
data/VERSION.yml
ADDED
data/lib/pomo.rb
ADDED
data/lib/pomo/po_file.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'pomo/translation'
|
2
|
+
|
3
|
+
module Pomo
|
4
|
+
class PoFile
|
5
|
+
def self.parse(text)
|
6
|
+
PoFile.new.add_translations_from_text(text)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.to_text(translations)
|
10
|
+
p = PoFile.new(:translations=>translations)
|
11
|
+
p.to_text
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.unique_translations(translations)
|
15
|
+
last_seen_at_index = {}
|
16
|
+
translations.each_with_index {|translation,index|last_seen_at_index[translation.msgid]=index}
|
17
|
+
last_seen_at_index.values.sort.map{|index| translations[index]}
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :translations
|
21
|
+
|
22
|
+
def initialize(options = {})
|
23
|
+
@translations = options[:translations] || []
|
24
|
+
end
|
25
|
+
|
26
|
+
#the text is split into lines and then converted into logical translations
|
27
|
+
#each translation consists of comments(that come before a translation)
|
28
|
+
#and a msgid / msgstr
|
29
|
+
def add_translations_from_text(text)
|
30
|
+
start_new_translation
|
31
|
+
text.split(/$/).each_with_index do |line,index|
|
32
|
+
@line_number = index + 1
|
33
|
+
next if line.empty?
|
34
|
+
if method_call? line
|
35
|
+
parse_method_call line
|
36
|
+
elsif comment? line
|
37
|
+
add_comment line
|
38
|
+
else
|
39
|
+
add_string line
|
40
|
+
end
|
41
|
+
end
|
42
|
+
start_new_translation #instance_variable has to be overwritten or errors can occur on next add
|
43
|
+
translations
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_text
|
47
|
+
self.class.unique_translations(translations).map {|translation|
|
48
|
+
comment = translation.comment.to_s.split(/\n|\r\n/).map{|line|"##{line}\n"}*''
|
49
|
+
msgid_and_msgstr = if translation.plural?
|
50
|
+
msgids =
|
51
|
+
%Q(msgid "#{translation.msgid[0]}"\n)+
|
52
|
+
%Q(msgid_plural "#{translation.msgid[1]}"\n)
|
53
|
+
|
54
|
+
msgstrs = []
|
55
|
+
translation.msgstr.each_with_index do |msgstr,index|
|
56
|
+
msgstrs << %Q(msgstr[#{index}] "#{msgstr}")
|
57
|
+
end
|
58
|
+
|
59
|
+
msgids + (msgstrs*"\n")
|
60
|
+
else
|
61
|
+
%Q(msgid "#{translation.msgid}"\n)+
|
62
|
+
%Q(msgstr "#{translation.msgstr}")
|
63
|
+
end
|
64
|
+
|
65
|
+
comment + msgid_and_msgstr
|
66
|
+
} * "\n\n"
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
#e.g. # fuzzy
|
72
|
+
def comment?(line)
|
73
|
+
line =~ /^\s*#/
|
74
|
+
end
|
75
|
+
|
76
|
+
def add_comment(line)
|
77
|
+
start_new_translation if translation_complete?
|
78
|
+
@current_translation.add_text(line.strip.sub('#','')+"\n",:to=>:comment)
|
79
|
+
end
|
80
|
+
|
81
|
+
#msgid "hello"
|
82
|
+
def method_call?(line)
|
83
|
+
line =~ /^\s*[a-z]/
|
84
|
+
end
|
85
|
+
|
86
|
+
#msgid "hello" -> method call msgid + add string "hello"
|
87
|
+
def parse_method_call(line)
|
88
|
+
method, string = line.match(/^\s*([a-z0-9_\[\]]+)(.*)/)[1..2]
|
89
|
+
raise "no method found" unless method
|
90
|
+
|
91
|
+
start_new_translation if method == 'msgid' and translation_complete?
|
92
|
+
@last_method = method.to_sym
|
93
|
+
add_string(string)
|
94
|
+
end
|
95
|
+
|
96
|
+
#"hello" -> hello
|
97
|
+
def add_string(string)
|
98
|
+
return if string.strip.empty?
|
99
|
+
raise "not string format: #{string.inspect} on line #{@line_number}" unless string.strip =~ /^['"](.*)['"]$/
|
100
|
+
@current_translation.add_text($1,:to=>@last_method)
|
101
|
+
end
|
102
|
+
|
103
|
+
def translation_complete?
|
104
|
+
return false unless @current_translation
|
105
|
+
@current_translation.complete?
|
106
|
+
end
|
107
|
+
|
108
|
+
def store_translation
|
109
|
+
@translations += [@current_translation] if @current_translation.complete?
|
110
|
+
end
|
111
|
+
|
112
|
+
def start_new_translation
|
113
|
+
store_translation if translation_complete?
|
114
|
+
@current_translation = Translation.new
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Pomo
|
2
|
+
class Translation
|
3
|
+
FUZZY_REGEX = /^\s*fuzzy\s*$/
|
4
|
+
attr_accessor :msgid, :msgstr, :comment
|
5
|
+
|
6
|
+
def add_text(text,options)
|
7
|
+
to = options[:to]
|
8
|
+
if to.to_sym == :msgid_plural
|
9
|
+
self.msgid = [msgid] unless msgid.is_a? Array
|
10
|
+
msgid[1] = msgid[1].to_s + text
|
11
|
+
elsif to.to_s =~ /^msgstr\[(\d)\]$/
|
12
|
+
self.msgstr ||= []
|
13
|
+
msgstr[$1.to_i] = msgstr[$1.to_i].to_s + text
|
14
|
+
else
|
15
|
+
#simple form
|
16
|
+
send("#{to}=",send(to).to_s+text)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_hash
|
21
|
+
{:msgid=>msgid,:msgstr=>msgstr,:comment=>comment}.reject{|k,value|value.nil?}
|
22
|
+
end
|
23
|
+
|
24
|
+
def complete?
|
25
|
+
not msgid.to_s.strip.empty? and not msgstr.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
def fuzzy?
|
29
|
+
comment =~ FUZZY_REGEX
|
30
|
+
end
|
31
|
+
|
32
|
+
def fuzzy=(value)
|
33
|
+
if value and not fuzzy?
|
34
|
+
add_text "\nfuzzy", :to=>:comment
|
35
|
+
else
|
36
|
+
self.comment = comment.to_s.split(/$/).reject{|line|line=~FUZZY_REGEX}.join("\n")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def plural?
|
41
|
+
msgid.is_a? Array or msgstr.is_a? Array
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require File.expand_path("../spec_helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
include Pomo
|
4
|
+
describe Pomo::PoFile do
|
5
|
+
it "parses nothing" do
|
6
|
+
PoFile.parse("").should be_empty
|
7
|
+
end
|
8
|
+
|
9
|
+
it "parses a simple msgid and msgstr" do
|
10
|
+
t = PoFile.parse(%Q(msgid "xxx"\nmsgstr "yyy"))
|
11
|
+
t[0].to_hash.should == {:msgid=>'xxx',:msgstr=>'yyy'}
|
12
|
+
end
|
13
|
+
|
14
|
+
it "parses a simple msgid and msg with additional whitespace" do
|
15
|
+
t = PoFile.parse(%Q( msgid "xxx" \n msgstr "yyy" ))
|
16
|
+
t[0].to_hash.should == {:msgid=>'xxx',:msgstr=>'yyy'}
|
17
|
+
end
|
18
|
+
|
19
|
+
it "parses a multiline msgid/msgstr" do
|
20
|
+
t = PoFile.parse(%Q(msgid "xxx"\n"aaa"\n\n\nmsgstr ""\n"bbb"))
|
21
|
+
t[0].to_hash.should == {:msgid=>'xxxaaa',:msgstr=>'bbb'}
|
22
|
+
end
|
23
|
+
|
24
|
+
it "parses simple comments" do
|
25
|
+
t = PoFile.parse(%Q(#test\nmsgid "xxx"\nmsgstr "yyy"))
|
26
|
+
t[0].to_hash.should == {:msgid=>'xxx',:msgstr=>'yyy',:comment=>"test\n"}
|
27
|
+
end
|
28
|
+
|
29
|
+
it "parses comments above msgstr" do
|
30
|
+
t = PoFile.parse(%Q(#test\nmsgid "xxx"\n#another\nmsgstr "yyy"))
|
31
|
+
t[0].to_hash.should == {:msgid=>'xxx',:msgstr=>'yyy',:comment=>"test\nanother\n"}
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "instance interface" do
|
35
|
+
it "adds two different translations" do
|
36
|
+
p = PoFile.new
|
37
|
+
p.add_translations_from_text(%Q(msgid "xxx"\nmsgstr "yyy"))
|
38
|
+
p.add_translations_from_text(%Q(msgid "aaa"\nmsgstr "yyy"))
|
39
|
+
p.translations[1].to_hash.should == {:msgid=>'aaa',:msgstr=>'yyy'}
|
40
|
+
end
|
41
|
+
|
42
|
+
it "can be initialized with translations" do
|
43
|
+
p = PoFile.new(:translations=>['xxx'])
|
44
|
+
p.translations[0].should == 'xxx'
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can be converted to text" do
|
48
|
+
text = %Q(msgid "xxx"\nmsgstr "aaa")
|
49
|
+
p = PoFile.new
|
50
|
+
p.add_translations_from_text(text)
|
51
|
+
p.to_text.should == text
|
52
|
+
end
|
53
|
+
|
54
|
+
it "keeps uniqueness when converting to_text" do
|
55
|
+
text = %Q(msgid "xxx"\nmsgstr "aaa")
|
56
|
+
p = PoFile.new
|
57
|
+
p.add_translations_from_text(%Q(msgid "xxx"\nmsgstr "yyy"))
|
58
|
+
p.add_translations_from_text(text)
|
59
|
+
p.to_text.should == text
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it "adds plural translations" do
|
64
|
+
t = PoFile.parse(%Q(msgid "singular"\nmsgid_plural "plural"\nmsgstr[0] "one"\nmsgstr[1] "many"))
|
65
|
+
t[0].to_hash.should == {:msgid=>['singular','plural'],:msgstr=>['one','many']}
|
66
|
+
end
|
67
|
+
|
68
|
+
it "does not fail on empty string" do
|
69
|
+
PoFile.parse(%Q(\n\n\n\n\n))
|
70
|
+
end
|
71
|
+
|
72
|
+
it "shows line number for invalid strings" do
|
73
|
+
begin
|
74
|
+
PoFile.parse(%Q(\n\n\n\n\nmsgstr "))
|
75
|
+
flunk
|
76
|
+
rescue Exception => e
|
77
|
+
e.to_s.should =~ /line 5/
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
describe :to_text do
|
82
|
+
it "is empty when not translations where added" do
|
83
|
+
PoFile.to_text([]).should == ""
|
84
|
+
end
|
85
|
+
|
86
|
+
it "preserves simple syntax" do
|
87
|
+
text = %Q(msgid "x"\nmsgstr "y")
|
88
|
+
PoFile.to_text(PoFile.parse(text)).should == text
|
89
|
+
end
|
90
|
+
|
91
|
+
it "adds comments" do
|
92
|
+
t = Pomo::Translation.new
|
93
|
+
t.msgid = 'a'
|
94
|
+
t.msgstr = 'b'
|
95
|
+
t.add_text("c\n",:to=>:comment)
|
96
|
+
t.add_text("d\n",:to=>:comment)
|
97
|
+
PoFile.to_text([t]).should == %Q(#c\n#d\nmsgid "a"\nmsgstr "b")
|
98
|
+
end
|
99
|
+
|
100
|
+
it "uses plural notation" do
|
101
|
+
text = %Q(#awesome\nmsgid "one"\nmsgid_plural "many"\nmsgstr[0] "1"\nmsgstr[1] "n")
|
102
|
+
PoFile.to_text(PoFile.parse(text)).should == text
|
103
|
+
end
|
104
|
+
|
105
|
+
it "only uses the latest of identicals msgids" do
|
106
|
+
text = %Q(msgid "one"\nmsgstr "1"\nmsgid "one"\nmsgstr "001")
|
107
|
+
PoFile.to_text(PoFile.parse(text)).should == %Q(msgid "one"\nmsgstr "001")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require File.expand_path("../spec_helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require 'pomo/translation'
|
4
|
+
|
5
|
+
describe Pomo::Translation do
|
6
|
+
describe :complete? do
|
7
|
+
it{should_not be_complete}
|
8
|
+
|
9
|
+
it "is complete if it has a msgid and a msgstr" do
|
10
|
+
subject.msgid="x"
|
11
|
+
subject.msgstr = "y"
|
12
|
+
should be_complete
|
13
|
+
end
|
14
|
+
|
15
|
+
it "is not complete if it has an complete msgid" do
|
16
|
+
subject.msgid=""
|
17
|
+
should_not be_complete
|
18
|
+
end
|
19
|
+
|
20
|
+
it "is not complete if it has a nil msgstr" do
|
21
|
+
subject.msgid="x"
|
22
|
+
should_not be_complete
|
23
|
+
end
|
24
|
+
|
25
|
+
it "is complete if it has an complete msgstr" do
|
26
|
+
subject.msgid = "x"
|
27
|
+
subject.msgstr = ""
|
28
|
+
should be_complete
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe :add_text do
|
33
|
+
it "adds a simple msgid" do
|
34
|
+
subject.add_text("x",:to=>:msgid)
|
35
|
+
subject.msgid.should == "x"
|
36
|
+
end
|
37
|
+
|
38
|
+
it "converts msgid to plural when msgid_plural is added" do
|
39
|
+
subject.add_text("x",:to=>:msgid)
|
40
|
+
subject.add_text("y",:to=>:msgid_plural)
|
41
|
+
subject.msgid.should == ["x",'y']
|
42
|
+
end
|
43
|
+
|
44
|
+
it "can add additional text to msgid plurals" do
|
45
|
+
subject.add_text("y",:to=>:msgid_plural)
|
46
|
+
subject.add_text("a",:to=>:msgid_plural)
|
47
|
+
subject.msgid.should == [nil,'ya']
|
48
|
+
end
|
49
|
+
|
50
|
+
it "can add plural msggstr" do
|
51
|
+
subject.add_text("x",:to=>'msgstr[0]')
|
52
|
+
subject.msgstr.should == ['x']
|
53
|
+
end
|
54
|
+
|
55
|
+
it "can add multiple plural msggstr" do
|
56
|
+
subject.add_text("x",:to=>'msgstr[0]')
|
57
|
+
subject.add_text("a",:to=>'msgstr[1]')
|
58
|
+
subject.add_text("y",:to=>'msgstr[1]')
|
59
|
+
subject.msgstr.should == ['x','ay']
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe :plural? do
|
64
|
+
it{should_not be_plural}
|
65
|
+
|
66
|
+
it "is plural if msgid is plural" do
|
67
|
+
subject.add_text("x",:to=>:msgid_plural)
|
68
|
+
should be_plural
|
69
|
+
end
|
70
|
+
|
71
|
+
it "is plural if msgstr is plural" do
|
72
|
+
subject.add_text("x",:to=>"msgstr[0]")
|
73
|
+
should be_plural
|
74
|
+
end
|
75
|
+
|
76
|
+
it "is not plural if simple strings where added" do
|
77
|
+
subject.msgid = "a"
|
78
|
+
subject.msgstr = "a"
|
79
|
+
should_not be_plural
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe :fuzzy? do
|
84
|
+
it{should_not be_fuzzy}
|
85
|
+
|
86
|
+
it "is fuzzy if a fuzzy comment was added" do
|
87
|
+
subject.add_text("fuzzy",:to=>:comment)
|
88
|
+
should be_fuzzy
|
89
|
+
end
|
90
|
+
|
91
|
+
it "can be made fuzzy by using fuzzy=" do
|
92
|
+
subject.fuzzy = true
|
93
|
+
should be_fuzzy
|
94
|
+
end
|
95
|
+
|
96
|
+
it "can be made unfuzzy by using fuzzy=" do
|
97
|
+
subject.fuzzy = false
|
98
|
+
should_not be_fuzzy
|
99
|
+
end
|
100
|
+
|
101
|
+
it "changes comment when made fuzzy through fuzzy=" do
|
102
|
+
subject.comment = "hello"
|
103
|
+
subject.fuzzy = true
|
104
|
+
subject.comment.should == "hello\nfuzzy"
|
105
|
+
end
|
106
|
+
|
107
|
+
it "changes empty comment when made fuzzy through fuzzy=" do
|
108
|
+
subject.fuzzy = true
|
109
|
+
subject.comment.should == "\nfuzzy"
|
110
|
+
end
|
111
|
+
|
112
|
+
it "preserves comments when making fuzzy/unfuzzy" do
|
113
|
+
subject.comment = "hello"
|
114
|
+
subject.fuzzy = true
|
115
|
+
subject.fuzzy = false
|
116
|
+
subject.comment.should == "hello"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grosser-pomo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Grosser
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-03-31 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: grosser.michael@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- VERSION.yml
|
26
|
+
- README.markdown
|
27
|
+
- lib/pomo
|
28
|
+
- lib/pomo/po_file.rb
|
29
|
+
- lib/pomo/translation.rb
|
30
|
+
- lib/pomo.rb
|
31
|
+
- spec/pomo
|
32
|
+
- spec/pomo/po_file_spec.rb
|
33
|
+
- spec/pomo/translation_spec.rb
|
34
|
+
- spec/spec_helper.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://github.com/grosser/pomo
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options:
|
39
|
+
- --inline-source
|
40
|
+
- --charset=UTF-8
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.2.0
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: "Ruby/Gettext: A .po and .mo file parser/generator"
|
62
|
+
test_files: []
|
63
|
+
|