PoParser 0.1.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.
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