get_pomo 0.6.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 @@
1
+ pkg
data/README.markdown ADDED
@@ -0,0 +1,47 @@
1
+ A simple and extendable .mo and .po file parser/generator.
2
+
3
+ Advanteges over original [mo](http://github.com/mutoh/gettext/blob/abf96713327cc4c5d35f0a772f3b75ff4819450c/lib/gettext/mofile.rb) / [po](http://github.com/mutoh/gettext/blob/abf96713327cc4c5d35f0a772f3b75ff4819450c/lib/gettext/poparser.rb)-parser:
4
+
5
+ - simple architecture + easy to extend/modify
6
+ - emtpy msgstr translations are read
7
+ - comments are included
8
+ - fuzzy can be set/unset
9
+ - multiple translations can be combined in a new po file(with comments and fuzzy and ...)
10
+ - po files can be written from any kind of input
11
+ - easy mo-file handling/merging
12
+ - po/mo file handling is identical, if you know one, you know both
13
+
14
+ Setup
15
+ =====
16
+ sudo gem install get_pomo -s http://gemcutter.org
17
+
18
+ ###Static interface
19
+ #parse po files
20
+ translations = GetPomo::PoFile.parse(File.read('xxx.po')) + GetPomo::PoFile.parse(File.read('yyy.po'))
21
+
22
+ #and use the data...
23
+ msgids = translations.reject{|t|t.plural? or t.fuzzy?}.map(&:msgid)
24
+
25
+ #or write a new po file (unique by msgid)...
26
+ File.open('xxx.po','w){|f|f.print(GetPomo::PoFile.to_text(translations))}
27
+
28
+
29
+ ###Instance interface
30
+ p = GetPomo::PoFile.new
31
+ p.add_translations_from_text(File.read('...'))
32
+ ...
33
+ p.translations
34
+ p.to_text
35
+
36
+ `GetPomo::MoFile` behaves identical.
37
+
38
+ TODO
39
+ ====
40
+ - extracting of version/pluralisation_rule/plurals/translator... (from msgid "")
41
+ - the vendor/mofile is really complex, maybe it can be refactored (also some parts are not needed)
42
+
43
+ Author
44
+ ======
45
+ [Michael Grosser](http://pragmatig.wordpress.com)
46
+ grosser.michael@gmail.com
47
+ Hereby placed under public domain, do what you want, just do not hold me accountable...
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ task :default => :spec
2
+ require 'spec/rake/spectask'
3
+ Spec::Rake::SpecTask.new {|t| t.spec_opts = ['--color']}
4
+
5
+ begin
6
+ project_name = 'get_pomo'
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = project_name
10
+ gem.summary = "Ruby/Gettext: A .po and .mo file parser/generator"
11
+ gem.email = "grosser.michael@gmail.com"
12
+ gem.homepage = "http://github.com/grosser/#{project_name}"
13
+ gem.authors = ["Michael Grosser"]
14
+ end
15
+
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
19
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.6.0
data/get_pomo.gemspec ADDED
@@ -0,0 +1,66 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{get_pomo}
8
+ s.version = "0.6.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Michael Grosser"]
12
+ s.date = %q{2009-11-20}
13
+ s.email = %q{grosser.michael@gmail.com}
14
+ s.extra_rdoc_files = [
15
+ "README.markdown"
16
+ ]
17
+ s.files = [
18
+ ".gitignore",
19
+ "README.markdown",
20
+ "Rakefile",
21
+ "VERSION",
22
+ "get_pomo.gemspec",
23
+ "lib/get_pomo.rb",
24
+ "lib/get_pomo/mo_file.rb",
25
+ "lib/get_pomo/po_file.rb",
26
+ "lib/get_pomo/translation.rb",
27
+ "prototype_treetop/po.treetop",
28
+ "prototype_treetop/test.rb",
29
+ "spec/files/complex.mo",
30
+ "spec/files/empty.mo",
31
+ "spec/files/plural.mo",
32
+ "spec/files/singular.mo",
33
+ "spec/files/singular_2.mo",
34
+ "spec/pomo/mo_file_spec.rb",
35
+ "spec/pomo/po_file_spec.rb",
36
+ "spec/pomo/translation_spec.rb",
37
+ "spec/pomo_spec.rb",
38
+ "spec/spec_helper.rb",
39
+ "vendor/README.rdoc",
40
+ "vendor/iconv.rb",
41
+ "vendor/mofile.rb"
42
+ ]
43
+ s.homepage = %q{http://github.com/grosser/get_pomo}
44
+ s.rdoc_options = ["--charset=UTF-8"]
45
+ s.require_paths = ["lib"]
46
+ s.rubygems_version = %q{1.3.5}
47
+ s.summary = %q{Ruby/Gettext: A .po and .mo file parser/generator}
48
+ s.test_files = [
49
+ "spec/spec_helper.rb",
50
+ "spec/pomo_spec.rb",
51
+ "spec/pomo/translation_spec.rb",
52
+ "spec/pomo/mo_file_spec.rb",
53
+ "spec/pomo/po_file_spec.rb"
54
+ ]
55
+
56
+ if s.respond_to? :specification_version then
57
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
58
+ s.specification_version = 3
59
+
60
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
61
+ else
62
+ end
63
+ else
64
+ end
65
+ end
66
+
@@ -0,0 +1,62 @@
1
+ require 'get_pomo/translation'
2
+ require File.join(File.dirname(__FILE__),'..','..','vendor','mofile')
3
+
4
+ module GetPomo
5
+ class MoFile
6
+ PLURAL_SEPERATOR = "\000"
7
+
8
+ def self.parse(text)
9
+ MoFile.new.add_translations_from_text(text)
10
+ end
11
+
12
+ def self.to_text(translations)
13
+ m = MoFile.new(:translations=>translations)
14
+ m.to_text
15
+ end
16
+
17
+ attr_reader :translations
18
+
19
+ def initialize(options = {})
20
+ @translations = options[:translations] || []
21
+ end
22
+
23
+ def add_translations_from_text(text)
24
+ text = StringIO.new(text)
25
+ @translations += GetPomo::GetText::MOFile.open(text, "UTF-8").map do |msgid,msgstr|
26
+ translation = Translation.new
27
+ if plural? msgid or plural? msgstr
28
+ translation.msgid = split_plural(msgid)
29
+ translation.msgstr = split_plural(msgstr)
30
+ else
31
+ translation.msgid = msgid
32
+ translation.msgstr = msgstr
33
+ end
34
+ translation
35
+ end
36
+ end
37
+
38
+ def to_text
39
+ m = GetPomo::GetText::MOFile.new
40
+ GetPomo.unique_translations(translations).each {|t| m[plural_to_string(t.msgid)] = plural_to_string(t.msgstr)}
41
+
42
+ io = StringIO.new
43
+ m.save_to_stream io
44
+ io.rewind
45
+ io.read
46
+ end
47
+
48
+ private
49
+
50
+ def plural_to_string(plural_or_singular)
51
+ [*plural_or_singular] * PLURAL_SEPERATOR
52
+ end
53
+
54
+ def plural? string
55
+ string.include? PLURAL_SEPERATOR
56
+ end
57
+
58
+ def split_plural string
59
+ string.split PLURAL_SEPERATOR
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,111 @@
1
+ require 'get_pomo/translation'
2
+
3
+ module GetPomo
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
+ attr_reader :translations
15
+
16
+ def initialize(options = {})
17
+ @translations = options[:translations] || []
18
+ end
19
+
20
+ #the text is split into lines and then converted into logical translations
21
+ #each translation consists of comments(that come before a translation)
22
+ #and a msgid / msgstr
23
+ def add_translations_from_text(text)
24
+ start_new_translation
25
+ text.split(/$/).each_with_index do |line,index|
26
+ @line_number = index + 1
27
+ next if line.empty?
28
+ if method_call? line
29
+ parse_method_call line
30
+ elsif comment? line
31
+ add_comment line
32
+ else
33
+ add_string line
34
+ end
35
+ end
36
+ start_new_translation #instance_variable has to be overwritten or errors can occur on next add
37
+ translations
38
+ end
39
+
40
+ def to_text
41
+ GetPomo.unique_translations(translations).map {|translation|
42
+ comment = translation.comment.to_s.split(/\n|\r\n/).map{|line|"##{line}\n"}*''
43
+ msgid_and_msgstr = if translation.plural?
44
+ msgids =
45
+ %Q(msgid "#{translation.msgid[0]}"\n)+
46
+ %Q(msgid_plural "#{translation.msgid[1]}"\n)
47
+
48
+ msgstrs = []
49
+ translation.msgstr.each_with_index do |msgstr,index|
50
+ msgstrs << %Q(msgstr[#{index}] "#{msgstr}")
51
+ end
52
+
53
+ msgids + (msgstrs*"\n")
54
+ else
55
+ %Q(msgid "#{translation.msgid}"\n)+
56
+ %Q(msgstr "#{translation.msgstr}")
57
+ end
58
+
59
+ comment + msgid_and_msgstr
60
+ } * "\n\n"
61
+ end
62
+
63
+ private
64
+
65
+ #e.g. # fuzzy
66
+ def comment?(line)
67
+ line =~ /^\s*#/
68
+ end
69
+
70
+ def add_comment(line)
71
+ start_new_translation if translation_complete?
72
+ @current_translation.add_text(line.strip.sub('#','')+"\n",:to=>:comment)
73
+ end
74
+
75
+ #msgid "hello"
76
+ def method_call?(line)
77
+ line =~ /^\s*[a-z]/
78
+ end
79
+
80
+ #msgid "hello" -> method call msgid + add string "hello"
81
+ def parse_method_call(line)
82
+ method, string = line.match(/^\s*([a-z0-9_\[\]]+)(.*)/)[1..2]
83
+ raise "no method found" unless method
84
+
85
+ start_new_translation if method == 'msgid' and translation_complete?
86
+ @last_method = method.to_sym
87
+ add_string(string)
88
+ end
89
+
90
+ #"hello" -> hello
91
+ def add_string(string)
92
+ return if string.strip.empty?
93
+ raise "not string format: #{string.inspect} on line #{@line_number}" unless string.strip =~ /^['"](.*)['"]$/
94
+ @current_translation.add_text($1,:to=>@last_method)
95
+ end
96
+
97
+ def translation_complete?
98
+ return false unless @current_translation
99
+ @current_translation.complete?
100
+ end
101
+
102
+ def store_translation
103
+ @translations += [@current_translation] if @current_translation.complete?
104
+ end
105
+
106
+ def start_new_translation
107
+ store_translation if translation_complete?
108
+ @current_translation = Translation.new
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,44 @@
1
+ module GetPomo
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.nil? 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
data/lib/get_pomo.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'get_pomo/po_file'
2
+ module GetPomo
3
+ VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
4
+
5
+ extend self
6
+
7
+ def self.unique_translations(translations)
8
+ last_seen_at_index = {}
9
+ translations.each_with_index {|translation,index|last_seen_at_index[translation.msgid]=index}
10
+ last_seen_at_index.values.sort.map{|index| translations[index]}
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ grammar Po
2
+ rule translation
3
+ msgid whitespace string
4
+ end
5
+
6
+ rule string
7
+ string whitespace string
8
+ end
9
+
10
+ rule quote
11
+ '"'
12
+ end
13
+
14
+ rule text
15
+ [^"]*
16
+ end
17
+
18
+ rule whitespace
19
+ [ \t\n\r]+
20
+ end
21
+
22
+ rule msgid
23
+ "msgid"
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'treetop'
3
+
4
+ Treetop.load "po"
5
+ parser = PoParser.new
6
+ puts parser.parse(%Q(msgid "xxx" "yyy"))
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,58 @@
1
+ require File.expand_path("../spec_helper", File.dirname(__FILE__))
2
+ require 'get_pomo/mo_file'
3
+
4
+ include GetPomo
5
+ describe GetPomo::MoFile do
6
+ it "parses empty mo file" do
7
+ MoFile.parse(File.read('spec/files/empty.mo')).should == []
8
+ end
9
+
10
+ it "parses empty strings" do
11
+ MoFile.parse(File.read('spec/files/empty.mo')).should == []
12
+ end
13
+
14
+ it "reads singulars" do
15
+ t = MoFile.parse(File.read('spec/files/singular.mo'))[0]
16
+ t.to_hash.should == {:msgid=>'Back',:msgstr=>'Zurück'}
17
+ end
18
+
19
+ it "reads plurals" do
20
+ t = MoFile.parse(File.read('spec/files/plural.mo'))[0]
21
+ t.to_hash.should == {:msgid=>['Axis','Axis'],:msgstr=>['Achse','Achsen']}
22
+ end
23
+
24
+ describe 'instance methods' do
25
+ it "combines multiple translations" do
26
+ m = MoFile.new
27
+ m.add_translations_from_text(File.read('spec/files/plural.mo'))
28
+ m.add_translations_from_text(File.read('spec/files/singular.mo'))
29
+ m.should have(2).translations
30
+ m.translations[0].msgid.should_not == m.translations[1].msgid
31
+ end
32
+
33
+ it "can be initialized with translations" do
34
+ m = MoFile.new(:translations=>['x'])
35
+ m.translations.should == ['x']
36
+ end
37
+
38
+ it "does not generate duplicate translations" do
39
+ second_version = File.read('spec/files/singular_2.mo')
40
+ m = MoFile.new
41
+ m.add_translations_from_text(File.read('spec/files/singular.mo'))
42
+ m.add_translations_from_text(second_version)
43
+ m.to_text.should == second_version
44
+ end
45
+ end
46
+
47
+ it "reads metadata" do
48
+ meta = MoFile.parse(File.read('spec/files/complex.mo')).detect {|t|t.msgid == ''}
49
+ meta.msgstr.should_not be_empty
50
+ end
51
+
52
+ describe :to_text do
53
+ it "writes singulars" do
54
+ text = File.read('spec/files/singular.mo')
55
+ MoFile.to_text(MoFile.parse(text)).should == text
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,117 @@
1
+ require File.expand_path("../spec_helper", File.dirname(__FILE__))
2
+
3
+ include GetPomo
4
+ describe GetPomo::PoFile do
5
+ describe :parse do
6
+ it "parses nothing" do
7
+ PoFile.parse("").should be_empty
8
+ end
9
+
10
+ it "parses a simple msgid and msgstr" do
11
+ t = PoFile.parse(%Q(msgid "xxx"\nmsgstr "yyy"))
12
+ t[0].to_hash.should == {:msgid=>'xxx',:msgstr=>'yyy'}
13
+ end
14
+
15
+ it "parses a simple msgid and msg with additional whitespace" do
16
+ t = PoFile.parse(%Q( msgid "xxx" \n msgstr "yyy" ))
17
+ t[0].to_hash.should == {:msgid=>'xxx',:msgstr=>'yyy'}
18
+ end
19
+
20
+ it "parses an empty msgid with text (gettext meta data)" do
21
+ t = PoFile.parse(%Q(msgid ""\nmsgstr "PLURAL FORMS"))
22
+ t[0].to_hash.should == {:msgid=>'',:msgstr=>'PLURAL FORMS'}
23
+ end
24
+
25
+ it "parses a multiline msgid/msgstr" do
26
+ t = PoFile.parse(%Q(msgid "xxx"\n"aaa"\n\n\nmsgstr ""\n"bbb"))
27
+ t[0].to_hash.should == {:msgid=>'xxxaaa',:msgstr=>'bbb'}
28
+ end
29
+
30
+ it "parses simple comments" do
31
+ t = PoFile.parse(%Q(#test\nmsgid "xxx"\nmsgstr "yyy"))
32
+ t[0].to_hash.should == {:msgid=>'xxx',:msgstr=>'yyy',:comment=>"test\n"}
33
+ end
34
+
35
+ it "parses comments above msgstr" do
36
+ t = PoFile.parse(%Q(#test\nmsgid "xxx"\n#another\nmsgstr "yyy"))
37
+ t[0].to_hash.should == {:msgid=>'xxx',:msgstr=>'yyy',:comment=>"test\nanother\n"}
38
+ end
39
+ end
40
+
41
+ describe "instance interface" do
42
+ it "adds two different translations" do
43
+ p = PoFile.new
44
+ p.add_translations_from_text(%Q(msgid "xxx"\nmsgstr "yyy"))
45
+ p.add_translations_from_text(%Q(msgid "aaa"\nmsgstr "yyy"))
46
+ p.translations[1].to_hash.should == {:msgid=>'aaa',:msgstr=>'yyy'}
47
+ end
48
+
49
+ it "can be initialized with translations" do
50
+ p = PoFile.new(:translations=>['xxx'])
51
+ p.translations[0].should == 'xxx'
52
+ end
53
+
54
+ it "can be converted to text" do
55
+ text = %Q(msgid "xxx"\nmsgstr "aaa")
56
+ p = PoFile.new
57
+ p.add_translations_from_text(text)
58
+ p.to_text.should == text
59
+ end
60
+
61
+ it "keeps uniqueness when converting to_text" do
62
+ text = %Q(msgid "xxx"\nmsgstr "aaa")
63
+ p = PoFile.new
64
+ p.add_translations_from_text(%Q(msgid "xxx"\nmsgstr "yyy"))
65
+ p.add_translations_from_text(text)
66
+ p.to_text.should == text
67
+ end
68
+ end
69
+
70
+ it "adds plural translations" do
71
+ t = PoFile.parse(%Q(msgid "singular"\nmsgid_plural "plural"\nmsgstr[0] "one"\nmsgstr[1] "many"))
72
+ t[0].to_hash.should == {:msgid=>['singular','plural'],:msgstr=>['one','many']}
73
+ end
74
+
75
+ it "does not fail on empty string" do
76
+ PoFile.parse(%Q(\n\n\n\n\n))
77
+ end
78
+
79
+ it "shows line number for invalid strings" do
80
+ begin
81
+ PoFile.parse(%Q(\n\n\n\n\nmsgstr "))
82
+ flunk
83
+ rescue Exception => e
84
+ e.to_s.should =~ /line 5/
85
+ end
86
+ end
87
+
88
+ describe :to_text do
89
+ it "is empty when not translations where added" do
90
+ PoFile.to_text([]).should == ""
91
+ end
92
+
93
+ it "preserves simple syntax" do
94
+ text = %Q(msgid "x"\nmsgstr "y")
95
+ PoFile.to_text(PoFile.parse(text)).should == text
96
+ end
97
+
98
+ it "adds comments" do
99
+ t = GetPomo::Translation.new
100
+ t.msgid = 'a'
101
+ t.msgstr = 'b'
102
+ t.add_text("c\n",:to=>:comment)
103
+ t.add_text("d\n",:to=>:comment)
104
+ PoFile.to_text([t]).should == %Q(#c\n#d\nmsgid "a"\nmsgstr "b")
105
+ end
106
+
107
+ it "uses plural notation" do
108
+ text = %Q(#awesome\nmsgid "one"\nmsgid_plural "many"\nmsgstr[0] "1"\nmsgstr[1] "n")
109
+ PoFile.to_text(PoFile.parse(text)).should == text
110
+ end
111
+
112
+ it "only uses the latest of identicals msgids" do
113
+ text = %Q(msgid "one"\nmsgstr "1"\nmsgid "one"\nmsgstr "001")
114
+ PoFile.to_text(PoFile.parse(text)).should == %Q(msgid "one"\nmsgstr "001")
115
+ end
116
+ end
117
+ end