grosser-pomo 0.4.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.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
|
+
|