PoParser 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 93ef3b40331e9ddb92b0e2e80cecdda8931c792d
4
+ data.tar.gz: fb04bd5618a796539cc22654c35f819cfaa1ba95
5
+ SHA512:
6
+ metadata.gz: 5db31ba15259828ab3fbd12f39ac9929ce3baa7244e2f51511ac9d46f31278c13aa40606af22c249e74d2ae3ccbc65b4996f9b0b0a62936dedaa2724260609ac
7
+ data.tar.gz: 351db54dfc9b1e1b75945c13a1ecc4ad7da2cd7c6d4a0e659466f904f1b319c538081042dbb4b790ffd6ebbf580e9686fa066dd510eaad2870cfdacac5ff9c35
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format doc
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.1.0
7
+ script: bundle exec rspec spec
data/CHANGELOG.md ADDED
@@ -0,0 +1,4 @@
1
+ ## Version 0.1.0
2
+
3
+ * initial release
4
+
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'coveralls', :require => false
4
+
5
+ # Specify your gem's dependencies in poparser.gemspec
6
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec, cmd: 'bundle exec rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Arash Mousavi
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,159 @@
1
+ # Poparser
2
+
3
+ [![Build Status](https://travis-ci.org/arashm/PoParser.svg?branch=master)](https://travis-ci.org/arashm/PoParser)
4
+ [![Coverage Status](https://img.shields.io/coveralls/arashm/PoParser.svg)](https://coveralls.io/r/arashm/PoParser)
5
+
6
+ A Ruby PO file parser, editor and generator. PO files are translation files generated by GNU/Gettext tool. This GEM is compatible with [GNU PO file specification](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html). report misbehaviours and bugs, to the [issue tracker](https://github.com/arashm/PoParser/issues).
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'poparser'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install poparser
21
+
22
+ ## Usage
23
+
24
+ Working with the GEM is pretty easy:
25
+
26
+ ```ruby
27
+ path = Pathname.new('example.po')
28
+ po = PoParser.parse(path)
29
+ => <PoParser::Po, Translated: 68.1% Untranslated: 20.4% Fuzzy: 11.5%>
30
+ ```
31
+
32
+ The `parse` method returns a `PO` object which contains all `Entries`:
33
+
34
+ ```ruby
35
+ # get all entries
36
+ po.entries # or .all alias
37
+
38
+ # get all fuzzy entries
39
+ po.fuzzy
40
+
41
+ # get all untranslated entries
42
+ po.untranslated
43
+
44
+ # get all translated entries
45
+ po.translated
46
+
47
+ # returns a hash representation of the PO file
48
+ po.to_h
49
+
50
+ # returns a string representation of the PO file
51
+ po.to_s
52
+ ```
53
+
54
+ You can add a new entry to the PO file:
55
+
56
+ ```ruby
57
+ new_entry = {
58
+ translator_comment: 'comment',
59
+ refrence: 'refrence comment',
60
+ msgid: 'untranslated',
61
+ msgstr: 'translated string'
62
+ }
63
+
64
+ po.add_entry(new_entry)
65
+
66
+ # There's also an alias for add_entry
67
+ po << new_entry
68
+ ```
69
+
70
+ You can pass an array of hashes to `new_entry` and it will be added to `PO` file.
71
+
72
+ ### Entry
73
+
74
+ Each entry can have following properties (for more information see [GNU PO file specification](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html)):
75
+
76
+ ```
77
+ translator_comment
78
+ refrence
79
+ extracted_comment
80
+ flag
81
+ previous_untraslated_string
82
+ msgid
83
+ msgid_plural
84
+ msgstr
85
+ msgctxt
86
+ ```
87
+
88
+ #### Working with entries
89
+
90
+ The `PO` object contains many `Entry` objects. Number of methods are available to check state of the `Entry`:
91
+
92
+ ```ruby
93
+ entry.untranslated? # or .incomplete? alias
94
+ #=> false
95
+ entry.translated? # or .complete? alias
96
+ #=> true
97
+ entry.fuzzy?
98
+ #=> true
99
+ entry.plural?
100
+ #=> false
101
+ ```
102
+
103
+ You can get or edit each of property of the `Entry`:
104
+
105
+ ```ruby
106
+ entry.msgid
107
+ #=> "This is an msgid that needs to get translated"
108
+ entry.translate = "This entry is translated" # or msgstr= alias
109
+ entry.msgstr
110
+ #=> "This entry is translated"
111
+ ```
112
+
113
+ You can mark an entry as fuzzy:
114
+
115
+ ```ruby
116
+ entry.flag_as_fuzzy
117
+ entry.fuzzy?
118
+ #=> true
119
+ ```
120
+
121
+ It's possible to get Hash and String representation of the `Entry`:
122
+
123
+ ```ruby
124
+ entry.to_h
125
+ entry.to_s(true)
126
+ ```
127
+
128
+ ### Searching
129
+
130
+ `PO` is an `Enumerable`. All exciting methods from `Enumerable` are available in `PO`. the `PO` yields `Entry`.
131
+
132
+ ### Saving
133
+ You can simply save the PO file using the `PO` object:
134
+
135
+ ```ruby
136
+ po.save_file
137
+ ```
138
+
139
+ If you want to save as the file in diffrent location change the `path`:
140
+
141
+ ```ruby
142
+ po.path
143
+ #=> example.po
144
+ po.path = 'example2.po'
145
+ po.save_file
146
+ ```
147
+
148
+ ##To-Do
149
+
150
+ * Streaming support
151
+ * Better error reporting
152
+
153
+ ## Contributing
154
+
155
+ 1. Fork it ( http://github.com/arashm/poparser/fork )
156
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
157
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
158
+ 4. Push to the branch (`git push origin my-new-feature`)
159
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,28 @@
1
+ module PoParser
2
+ class Comment
3
+ attr_accessor :type, :str
4
+
5
+ def initialize(type, str)
6
+ @type = type
7
+ @str = str
8
+ end
9
+
10
+ def to_s(with_label = false)
11
+ return @str unless with_label
12
+ if @str.is_a? Array
13
+ string = []
14
+ @str.each do |str|
15
+ string << "#{COMMENTS_LABELS[@type]} #{str}\n".gsub(/[^\S\n]+$/, '')
16
+ end
17
+ return string.join
18
+ else
19
+ # removes the space but not newline at the end
20
+ "#{COMMENTS_LABELS[@type]} #{@str}\n".gsub(/[^\S\n]+$/, '')
21
+ end
22
+ end
23
+
24
+ def to_str
25
+ @str
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ module PoParser
2
+ COMMENTS_LABELS = {
3
+ :translator_comment => '#',
4
+ :refrence => '#:',
5
+ :extracted_comment => '#.',
6
+ :flag => '#,',
7
+ :previous_untraslated_string => '#|',
8
+ }
9
+
10
+ ENTRIES_LABELS = {
11
+ :msgid => 'msgid',
12
+ :msgid_plural => 'msgid_plural',
13
+ :msgstr => 'msgstr',
14
+ :msgctxt => 'msgctxt'
15
+ }
16
+
17
+ LABELS = COMMENTS_LABELS.merge(ENTRIES_LABELS).keys
18
+ end
@@ -0,0 +1,154 @@
1
+ module PoParser
2
+ class Entry
3
+ # TODO: raise error if a label is not known
4
+ def initialize(args= {})
5
+ # Defining all instance variables to prevent warnings
6
+ LABELS.each do |label|
7
+ instance_variable_set "@#{label.to_s}".to_sym, nil
8
+ end
9
+
10
+ # Set passed arguments
11
+ args.each do |type, string|
12
+ if COMMENTS_LABELS.include? type
13
+ instance_variable_set "@#{type.to_s}".to_sym, Comment.new(type, string)
14
+ elsif ENTRIES_LABELS.include? type
15
+ instance_variable_set "@#{type.to_s}".to_sym, Message.new(type, string)
16
+ elsif type.to_s.match(/^msgstr\[[0-9]\]/)
17
+ # If it's a plural msgstr
18
+ @msgstr ||= []
19
+ @msgstr << Message.new(type, string)
20
+ end
21
+ end
22
+
23
+ define_writer_methods
24
+ define_reader_methods
25
+ end
26
+
27
+ # Checks if the entry is untraslated
28
+ #
29
+ # @return [Boolean]
30
+ def untranslated?
31
+ @msgstr.nil? || @msgstr.to_s == ''
32
+ end
33
+ alias_method :incomplete? , :untranslated?
34
+
35
+ # Checks if the entry is translated
36
+ #
37
+ # @return [Boolean]
38
+ def translated?
39
+ not untranslated?
40
+ end
41
+ alias_method :complete? , :translated?
42
+
43
+ # Checks if the entry is plural
44
+ #
45
+ # @return [Boolean]
46
+ def plural?
47
+ @msgid_plural != nil
48
+ end
49
+
50
+ # Checks if the entry is fuzzy
51
+ #
52
+ # @return [Boolean]
53
+ def fuzzy?
54
+ @flag.to_s == 'fuzzy'
55
+ end
56
+
57
+ # Flag the entry as Fuzzy
58
+ # @return [Entry]
59
+ def flag_as_fuzzy
60
+ @flag = 'fuzzy'
61
+ self
62
+ end
63
+
64
+ # Set flag to a custome string
65
+ def flag_as(flag)
66
+ raise ArgumentError if flag.class != String
67
+ @flag = flag
68
+ end
69
+
70
+ # Convert entry to a hash key value
71
+ # @return [Hash]
72
+ def to_h
73
+ hash = {}
74
+ instance_variables.each do |label|
75
+ object = instance_variable_get(label)
76
+ # If it's a plural msgstr
77
+ if object.is_a? Array
78
+ object.each do |entry|
79
+ hash[entry.type] = entry.to_s if not entry.nil?
80
+ end
81
+ else
82
+ hash[object.type] = object.to_s if not object.nil?
83
+ end
84
+ end
85
+ hash
86
+ end
87
+
88
+ # Convert entry to a string
89
+ # @return [String]
90
+ def to_s
91
+ lines = []
92
+ LABELS.each do |label|
93
+ object = instance_variable_get("@#{label}".to_sym)
94
+ # If it's a plural msgstr
95
+ if object.is_a? Array
96
+ object.each do |entry|
97
+ lines << entry.to_s(true) if not entry.nil?
98
+ end
99
+ else
100
+ lines << object.to_s(true) if not object.nil?
101
+ end
102
+ end
103
+
104
+ lines.join
105
+ end
106
+
107
+ private
108
+
109
+ def define_writer_methods
110
+ COMMENTS_LABELS.each do |type, mark|
111
+ unless Entry.method_defined? "#{type}=".to_sym
112
+ self.class.send(:define_method, "#{type}=".to_sym, lambda { |val|
113
+ if instance_variable_get("@#{type}".to_sym).is_a? Comment
114
+ comment = instance_variable_get "@#{type}".to_sym
115
+ comment.type = type
116
+ comment.str = val
117
+ else
118
+ instance_variable_set "@#{type}".to_sym, Comment.new(type, val)
119
+ end
120
+ instance_variable_get "@#{type}".to_sym
121
+ })
122
+ end
123
+ end
124
+
125
+ ENTRIES_LABELS.each do |type, mark|
126
+ unless Entry.method_defined? "#{type}=".to_sym
127
+ self.class.send(:define_method, "#{type}=".to_sym, lambda { |val|
128
+ if instance_variable_get("@#{type}".to_sym).is_a? Message
129
+ message = instance_variable_get "@#{type}".to_sym
130
+ message.type = type
131
+ message.str = val
132
+ else
133
+ instance_variable_set "@#{type}".to_sym, Message.new(type, val)
134
+ end
135
+ instance_variable_get "@#{type}".to_sym
136
+ })
137
+ end
138
+ end
139
+
140
+ self.class.send(:alias_method, :translate, :msgstr=)
141
+ end
142
+
143
+ def define_reader_methods
144
+ LABELS.each do |label|
145
+ unless Entry.method_defined? "#{label}".to_sym
146
+ self.class.send(:define_method, label.to_sym) do
147
+ instance_variable_get "@#{label}".to_sym
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ end
154
+ end
@@ -0,0 +1,51 @@
1
+ module PoParser
2
+ class Message
3
+ attr_accessor :type
4
+ attr_writer :str
5
+
6
+ def initialize(type, str)
7
+ @type = type
8
+ @str = str
9
+
10
+ remove_empty_line
11
+ end
12
+
13
+ def str
14
+ @str.join
15
+ end
16
+
17
+ def to_s(with_label = false)
18
+ return @str unless with_label
19
+ if @str.is_a? Array
20
+ remove_empty_line
21
+ # multiline messages should be started with an empty line
22
+ lines = ["#{label} \"\"\n"]
23
+ @str.each do |str|
24
+ lines << "\"#{str}\"\n"
25
+ end
26
+ return lines.join
27
+ else
28
+ "#{label} \"#{@str}\"\n"
29
+ end
30
+ end
31
+
32
+ def to_str
33
+ @str.join
34
+ end
35
+
36
+ private
37
+ def remove_empty_line
38
+ if @str.is_a? Array
39
+ @str.shift if @str.first == ''
40
+ end
41
+ end
42
+
43
+ def label
44
+ if @type.to_s.match(/msgstr\[[0-9]\]/)
45
+ @type
46
+ else
47
+ ENTRIES_LABELS[@type]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,66 @@
1
+ module PoParser
2
+ class Parser < Parslet::Parser
3
+ root(:document)
4
+
5
+ rule(:document) { lines.repeat }
6
+ rule(:lines) { comments | entries }
7
+
8
+ # Comments
9
+ rule(:comments) do
10
+ refrence |
11
+ extracted_comment | flag |
12
+ previous_untraslated_string |
13
+ translator_comment
14
+ end
15
+
16
+ rule(:translator_comment) { spaced('#') >> comment_text_line.as(:translator_comment) }
17
+ rule(:extracted_comment) { spaced('#.') >> comment_text_line.as(:extracted_comment) }
18
+ rule(:refrence) { spaced('#:') >> comment_text_line.as(:refrence) }
19
+ rule(:flag) { spaced('#,') >> comment_text_line.as(:flag) }
20
+ rule(:previous_untraslated_string){ spaced('#|') >> comment_text_line.as(:previous_untraslated_string) }
21
+
22
+ # Entries
23
+ rule(:entries) do
24
+ msgid.as(:msgid) |
25
+ msgid_plural.as(:msgid_plural) |
26
+ msgstr.as(:msgstr) |
27
+ msgstr_plural.as(:msgstr_plural) |
28
+ msgctxt.as(:msgctxt)
29
+ end
30
+
31
+ rule(:multiline) { str('"').present? >> msg_text_line.repeat.maybe }
32
+ rule(:msgid) { spaced('msgid') >> msg_text_line >> multiline.repeat }
33
+ rule(:msgid_plural) { spaced('msgid_plural') >> msg_text_line >> multiline.repeat }
34
+
35
+ rule(:msgstr) { spaced('msgstr') >> msg_text_line >> multiline.repeat }
36
+ rule(:msgstr_plural){ str('msgstr') >> bracketed(match["[0-9]"].as(:plural_id)) >> space? >> msg_text_line >> multiline.repeat }
37
+ rule(:msgctxt) { spaced('msgctxt') >> msg_text_line >> multiline.repeat }
38
+
39
+ # Helpers
40
+ rule(:space) { match['[^\S\n]'] } #match only whitespace and not newline
41
+ rule(:space?) { space.maybe }
42
+ rule(:newline) { match["\n"] }
43
+ rule(:eol) { newline | any.absent? }
44
+ rule(:character) { escaped | text }
45
+ rule(:text) { any }
46
+ rule(:escaped) { str('\\') >> any }
47
+ rule(:msg_line_end){ str('"') >> eol }
48
+
49
+ rule(:comment_text_line) do
50
+ (eol.absent? >> character).repeat.maybe.as(:text) >> eol
51
+ end
52
+
53
+ rule(:msg_text_line) do
54
+ str('"') >> (msg_line_end.absent? >> character).repeat.maybe.as(:text) >> msg_line_end
55
+ end
56
+
57
+ def bracketed(atom)
58
+ str('[') >> atom >> str(']')
59
+ end
60
+
61
+ def spaced(character)
62
+ str(character) >> space?
63
+ end
64
+ end
65
+ end
66
+
@@ -0,0 +1,134 @@
1
+ module PoParser
2
+ # Po class keeps all entries of a Po file
3
+ #
4
+ class Po
5
+ include Enumerable
6
+ attr_reader :entries
7
+ attr_accessor :path
8
+ alias_method :all, :entries
9
+
10
+ def initialize(args = {})
11
+ @entries = []
12
+ @path = args.fetch(:path, nil)
13
+ end
14
+
15
+ # add new entries to po file
16
+ #
17
+ # @example
18
+ # entry = {
19
+ # translator_comment: 'comment',
20
+ # refrence: 'refrense comment',
21
+ # flag: 'fuzzy',
22
+ # msgstr: 'translatable string',
23
+ # msgstr: 'translation'
24
+ # }
25
+ # add_entry(entry)
26
+ #
27
+ # @param entry [Hash, Array] a hash of entry contents or an array of hashes
28
+ def add_entry(entry)
29
+ if entry.kind_of? Hash
30
+ @entries << Entry.new(entry)
31
+ @entries.last
32
+ elsif entry.kind_of? Array
33
+ entry.each do |en|
34
+ @entries << Entry.new(en)
35
+ end
36
+ else
37
+ raise ArgumentError, 'Must be a hash or an array of hashes'
38
+ end
39
+ end
40
+ alias_method :<<, :add_entry
41
+
42
+ # Finds all entries that are flaged as fuzzy
43
+ #
44
+ # @return [Array] an array of fuzzy entries
45
+ def fuzzy
46
+ find_all do |entry|
47
+ entry.fuzzy?
48
+ end
49
+ end
50
+
51
+ # Finds all entries that are untranslated
52
+ #
53
+ # @return [Array] an array of untranslated entries
54
+ def untranslated
55
+ find_all do |entry|
56
+ entry.untranslated?
57
+ end
58
+ end
59
+
60
+ # Finds all entries that are translated
61
+ #
62
+ # @return [Array] an array of translated entries
63
+ def translated
64
+ find_all do |entry|
65
+ entry.translated?
66
+ end
67
+ end
68
+
69
+ # Shows statistics and status of the provided file in percentage.
70
+ #
71
+ # @return [Hash] a hash of translated, untranslated and fuzzy percentages
72
+ def stats
73
+ untranslated_size = untranslated.size
74
+ translated_size = translated.size
75
+ fuzzy_size = fuzzy.size
76
+
77
+ {
78
+ translated: percentage(translated_size - fuzzy_size),
79
+ untranslated: percentage(untranslated_size),
80
+ fuzzy: percentage(fuzzy_size)
81
+ }
82
+ end
83
+
84
+ # Converts Po file to an hashes of entries
85
+ #
86
+ # @return [Array] array of hashes of entries
87
+ def to_h
88
+ array = []
89
+ @entries.each do |entry|
90
+ array << entry.to_h
91
+ end
92
+ array
93
+ end
94
+
95
+ # Shows a String representation of the Po file
96
+ #
97
+ # @return [String]
98
+ def to_s
99
+ array = []
100
+ @entries.each do |entry|
101
+ array << entry.to_s
102
+ end
103
+ array.join("\n")
104
+ end
105
+
106
+ # Saves the file to the provided path
107
+ def save_file
108
+ raise ArgumentError, 'Need a Path to save the file' if @path.nil?
109
+ File.open(@path, 'w') do |f|
110
+ f.write to_s
111
+ end
112
+ end
113
+
114
+ def each
115
+ @entries.each do |entry|
116
+ yield entry
117
+ end
118
+ end
119
+
120
+ def inspect
121
+ "<#{self.class.name}, Translated: #{stats[:translated]}% Untranslated: #{stats[:untranslated]}% Fuzzy: #{stats[:fuzzy]}%>"
122
+ end
123
+
124
+ private
125
+ # calculates percentages based on total number of entries
126
+ #
127
+ # @param [Integer] number of entries
128
+ # @return [Float] percentage of the provided entries
129
+ def percentage(size)
130
+ total = @entries.size
131
+ ((size.to_f / total) * 100).round(1)
132
+ end
133
+ end
134
+ end