gettext 2.3.3 → 2.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/doc/text/news.md +37 -0
  2. data/gettext.gemspec +1 -0
  3. data/lib/gettext/runtime/mo.rb +382 -0
  4. data/lib/gettext/runtime/mofile.rb +24 -366
  5. data/lib/gettext/runtime/textdomain.rb +17 -17
  6. data/lib/gettext/tools.rb +1 -1
  7. data/lib/gettext/tools/msgfmt.rb +2 -2
  8. data/lib/gettext/tools/msginit.rb +16 -15
  9. data/lib/gettext/tools/msgmerge.rb +258 -255
  10. data/lib/gettext/tools/parser/ruby.rb +24 -24
  11. data/lib/gettext/tools/po.rb +256 -0
  12. data/lib/gettext/tools/po_entry.rb +355 -0
  13. data/lib/gettext/tools/poparser.rb +118 -16
  14. data/lib/gettext/tools/xgettext.rb +56 -58
  15. data/lib/gettext/version.rb +1 -1
  16. data/samples/po/hello.pot +3 -3
  17. data/samples/po/hello2.pot +3 -3
  18. data/samples/po/hello_glade2.pot +3 -3
  19. data/samples/po/hello_gtk2.pot +3 -3
  20. data/samples/po/hello_noop.pot +3 -3
  21. data/samples/po/hello_plural.pot +3 -3
  22. data/samples/po/hello_tk.pot +3 -3
  23. data/src/poparser.ry +111 -9
  24. data/test/parser/test_ruby.rb +17 -13
  25. data/test/po/_.pot +3 -3
  26. data/test/po/backslash.pot +3 -3
  27. data/test/po/non_ascii.pot +3 -3
  28. data/test/po/np_.pot +5 -4
  29. data/test/po/ns_.pot +3 -3
  30. data/test/po/p_.pot +3 -3
  31. data/test/po/s_.pot +3 -3
  32. data/test/po/untranslated.pot +3 -3
  33. data/test/{test_mofile.rb → test_mo.rb} +3 -3
  34. data/test/test_parser.rb +13 -12
  35. data/test/test_po_entry.rb +329 -0
  36. data/test/test_po_parser.rb +209 -8
  37. data/test/tools/test_msginit.rb +0 -2
  38. data/test/tools/test_msgmerge.rb +427 -50
  39. data/test/tools/test_po.rb +487 -0
  40. data/test/tools/test_xgettext.rb +1 -1
  41. metadata +28 -45
  42. data/data/locale/de/LC_MESSAGES/gettext.mo +0 -0
  43. data/data/locale/de/LC_MESSAGES/rgettext.mo +0 -0
  44. data/data/locale/el/LC_MESSAGES/gettext.mo +0 -0
  45. data/data/locale/el/LC_MESSAGES/rgettext.mo +0 -0
  46. data/data/locale/sr/LC_MESSAGES/gettext.mo +0 -0
  47. data/data/locale/sr/LC_MESSAGES/rgettext.mo +0 -0
  48. data/data/locale/uk/LC_MESSAGES/gettext.mo +0 -0
  49. data/data/locale/uk/LC_MESSAGES/rgettext.mo +0 -0
  50. data/lib/gettext/tools/pomessage.rb +0 -232
  51. data/samples/locale/bg/LC_MESSAGES/hello_gtk.mo +0 -0
  52. data/samples/locale/bs/LC_MESSAGES/hello_gtk.mo +0 -0
  53. data/samples/locale/ca/LC_MESSAGES/hello_gtk.mo +0 -0
  54. data/samples/locale/cs/LC_MESSAGES/hello_gtk.mo +0 -0
  55. data/samples/locale/de/LC_MESSAGES/hello_gtk.mo +0 -0
  56. data/samples/locale/el/LC_MESSAGES/hello_gtk.mo +0 -0
  57. data/samples/locale/eo/LC_MESSAGES/hello_gtk.mo +0 -0
  58. data/samples/locale/es/LC_MESSAGES/hello_gtk.mo +0 -0
  59. data/samples/locale/fr/LC_MESSAGES/hello_gtk.mo +0 -0
  60. data/samples/locale/hr/LC_MESSAGES/hello_gtk.mo +0 -0
  61. data/samples/locale/hu/LC_MESSAGES/hello_gtk.mo +0 -0
  62. data/samples/locale/it/LC_MESSAGES/hello_gtk.mo +0 -0
  63. data/samples/locale/ja/LC_MESSAGES/hello_gtk.mo +0 -0
  64. data/samples/locale/ko/LC_MESSAGES/hello_gtk.mo +0 -0
  65. data/samples/locale/lv/LC_MESSAGES/hello_gtk.mo +0 -0
  66. data/samples/locale/nb/LC_MESSAGES/hello_gtk.mo +0 -0
  67. data/samples/locale/nl/LC_MESSAGES/hello_gtk.mo +0 -0
  68. data/samples/locale/pt_BR/LC_MESSAGES/hello_gtk.mo +0 -0
  69. data/samples/locale/ru/LC_MESSAGES/hello_gtk.mo +0 -0
  70. data/samples/locale/sr/LC_MESSAGES/hello_gtk.mo +0 -0
  71. data/samples/locale/sv/LC_MESSAGES/hello_gtk.mo +0 -0
  72. data/samples/locale/uk/LC_MESSAGES/hello_gtk.mo +0 -0
  73. data/samples/locale/vi/LC_MESSAGES/hello_gtk.mo +0 -0
  74. data/samples/locale/zh/LC_MESSAGES/hello_gtk.mo +0 -0
  75. data/samples/locale/zh_TW/LC_MESSAGES/hello_gtk.mo +0 -0
  76. data/test/po/ascii.pot +0 -23
  77. data/test/po/no_exist_msgid.pot +0 -20
  78. data/test/po/not_existed_msgid.pot +0 -20
  79. data/test/test_po_message.rb +0 -118
data/doc/text/news.md CHANGED
@@ -1,3 +1,40 @@
1
+ # News
2
+ ## <a id="2-3-4">2.3.4</a>: 2012-12-11
3
+
4
+ This is a many changes and new implements release.
5
+
6
+ ### Improvements
7
+
8
+ * [Merger] Implemented "fuzzy-match" with Levenshtein distance.
9
+ * Added the class "PO" for management PO entries. Please use PO
10
+ instead of PoData. (see details in
11
+ http://rubydoc.info/gems/gettext/GetText/PO.html)
12
+ * [POEntry (renamed from PoMessages)] Supported to specify msgstr.
13
+ * [POEntry] Stored comments each type
14
+ (translator\_comment, extracted\_comment, flag, previous).
15
+ see
16
+ http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
17
+ for details of comment type.
18
+ * [POEntry] Checked if specified type is valid in #type=.
19
+ * [PoParser][MO] Concatenated msgctxt, msgid, msgid\_plural to
20
+ "#{msgctxt}\004#{msgid}\000"{msgid\_plural}" by MO instead of
21
+ PoParser. PoData and MO treat a concatenated string as msgid, but
22
+ PO doesn't.
23
+ * [PoParser] Parsed each type comment from whole comment.
24
+
25
+ ### Changes
26
+
27
+ * Rename some classes and methods.
28
+ * PoMessage to PoEntry. This isn't "message" but "entry".
29
+ (See http://www.gnu.org/software/gettext/manual/gettext.html#PO-Files)
30
+ * PoMessages#== to POEntry#mergeable?.
31
+ * PoMessages#to\_po\_str to POEntry#to\_s.
32
+ * PoMessages#sources(sources=) to POEntry#references(references=)
33
+ * MoFile to MO. For backword compatible, MoFile can be used now.
34
+ * PoParser to POParser. For backword compatible, PoParser can be used now.
35
+ * Raised no error when POEntry doesn't have references.
36
+ It is useful for no references in .PO file.
37
+
1
38
  # News
2
39
  ## <a id="2-3-3">2.3.3</a>: 2012-10-18
3
40
 
data/gettext.gemspec CHANGED
@@ -29,6 +29,7 @@ So you can use GNU gettext tools for maintaining.
29
29
  end
30
30
 
31
31
  s.add_runtime_dependency("locale")
32
+ s.add_runtime_dependency("levenshtein")
32
33
  s.add_development_dependency("rake")
33
34
  s.add_development_dependency("racc")
34
35
  s.add_development_dependency("yard")
@@ -0,0 +1,382 @@
1
+ # encoding: utf-8
2
+
3
+ =begin
4
+ mo.rb - A simple class for operating GNU MO file.
5
+
6
+ Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
7
+ Copyright (C) 2003-2009 Masao Mutoh
8
+ Copyright (C) 2002 Masahiro Sakai, Masao Mutoh
9
+ Copyright (C) 2001 Masahiro Sakai
10
+
11
+ Masahiro Sakai <s01397ms at sfc.keio.ac.jp>
12
+ Masao Mutoh <mutomasa at gmail.com>
13
+
14
+ You can redistribute this file and/or modify it under the same term
15
+ of Ruby. License of Ruby is included with Ruby distribution in
16
+ the file "README".
17
+
18
+ =end
19
+
20
+ require 'stringio'
21
+
22
+ module GetText
23
+ class MO < Hash
24
+ class InvalidFormat < RuntimeError; end;
25
+
26
+ attr_reader :filename
27
+
28
+ Header = Struct.new(:magic,
29
+ :revision,
30
+ :nstrings,
31
+ :orig_table_offset,
32
+ :translated_table_offset,
33
+ :hash_table_size,
34
+ :hash_table_offset)
35
+
36
+ # The following are only used in .mo files
37
+ # with minor revision >= 1.
38
+ class HeaderRev1 < Header
39
+ attr_accessor :n_sysdep_segments,
40
+ :sysdep_segments_offset,
41
+ :n_sysdep_strings,
42
+ :orig_sysdep_tab_offset,
43
+ :trans_sysdep_tab_offset
44
+ end
45
+
46
+ MAGIC_BIG_ENDIAN = "\x95\x04\x12\xde"
47
+ MAGIC_LITTLE_ENDIAN = "\xde\x12\x04\x95"
48
+ if "".respond_to?(:force_encoding)
49
+ MAGIC_BIG_ENDIAN.force_encoding("ASCII-8BIT")
50
+ MAGIC_LITTLE_ENDIAN.force_encoding("ASCII-8BIT")
51
+ end
52
+
53
+ def self.open(arg = nil, output_charset = nil)
54
+ result = self.new(output_charset)
55
+ result.load(arg)
56
+ end
57
+
58
+ def initialize(output_charset = nil)
59
+ @filename = nil
60
+ @last_modified = nil
61
+ @little_endian = true
62
+ @output_charset = output_charset
63
+ @plural_proc = nil
64
+ super()
65
+ end
66
+
67
+ def store(msgid, msgstr, options)
68
+ string = generate_original_string(msgid, options)
69
+ self[string] = msgstr
70
+ end
71
+
72
+ def update!
73
+ if FileTest.exist?(@filename)
74
+ st = File.stat(@filename)
75
+ load(@filename) unless (@last_modified == [st.ctime, st.mtime])
76
+ else
77
+ warn "#{@filename} was lost." if $DEBUG
78
+ clear
79
+ end
80
+ self
81
+ end
82
+
83
+ def load(arg)
84
+ if arg.kind_of? String
85
+ begin
86
+ st = File.stat(arg)
87
+ @last_modified = [st.ctime, st.mtime]
88
+ rescue Exception
89
+ end
90
+ load_from_file(arg)
91
+ else
92
+ load_from_stream(arg)
93
+ end
94
+ @filename = arg
95
+ self
96
+ end
97
+
98
+ def load_from_stream(io)
99
+ magic = io.read(4)
100
+ case magic
101
+ when MAGIC_BIG_ENDIAN
102
+ @little_endian = false
103
+ when MAGIC_LITTLE_ENDIAN
104
+ @little_endian = true
105
+ else
106
+ raise InvalidFormat.new(sprintf("Unknown signature %s", magic.dump))
107
+ end
108
+
109
+ endian_type6 = @little_endian ? 'V6' : 'N6'
110
+ endian_type_astr = @little_endian ? 'V*' : 'N*'
111
+
112
+ header = HeaderRev1.new(magic, *(io.read(4 * 6).unpack(endian_type6)))
113
+
114
+ if header.revision == 1
115
+ # FIXME: It doesn't support sysdep correctly.
116
+ header.n_sysdep_segments = io.read(4).unpack(endian_type6)
117
+ header.sysdep_segments_offset = io.read(4).unpack(endian_type6)
118
+ header.n_sysdep_strings = io.read(4).unpack(endian_type6)
119
+ header.orig_sysdep_tab_offset = io.read(4).unpack(endian_type6)
120
+ header.trans_sysdep_tab_offset = io.read(4).unpack(endian_type6)
121
+ elsif header.revision > 1
122
+ raise InvalidFormat.new(sprintf("file format revision %d isn't supported", header.revision))
123
+ end
124
+ io.pos = header.orig_table_offset
125
+ orig_table_data = io.read((4 * 2) * header.nstrings).unpack(endian_type_astr)
126
+
127
+ io.pos = header.translated_table_offset
128
+ trans_table_data = io.read((4 * 2) * header.nstrings).unpack(endian_type_astr)
129
+
130
+ msgids = Array.new(header.nstrings)
131
+ for i in 0...header.nstrings
132
+ io.pos = orig_table_data[i * 2 + 1]
133
+ msgids[i] = io.read(orig_table_data[i * 2 + 0])
134
+ end
135
+
136
+ clear
137
+ for i in 0...header.nstrings
138
+ io.pos = trans_table_data[i * 2 + 1]
139
+ msgstr = io.read(trans_table_data[i * 2 + 0])
140
+
141
+ msgid = msgids[i]
142
+ if msgid.nil? || msgid.empty?
143
+ if msgstr
144
+ @charset = nil
145
+ @nplurals = nil
146
+ @plural = nil
147
+ msgstr.each_line{|line|
148
+ if /^Content-Type:/i =~ line and /charset=((?:\w|-)+)/i =~ line
149
+ @charset = $1
150
+ elsif /^Plural-Forms:\s*nplurals\s*\=\s*(\d*);\s*plural\s*\=\s*([^;]*)\n?/ =~ line
151
+ @nplurals = $1
152
+ @plural = $2
153
+ end
154
+ break if @charset and @nplurals
155
+ }
156
+ @nplurals = "1" unless @nplurals
157
+ @plural = "0" unless @plural
158
+ end
159
+ else
160
+ unless msgstr.nil?
161
+ msgstr = convert_encoding(msgstr, msgid)
162
+ end
163
+ end
164
+ self[convert_encoding(msgid, msgid)] = msgstr.freeze
165
+ end
166
+ self
167
+ end
168
+
169
+ def prime?(number)
170
+ ('1' * number) !~ /^1?$|^(11+?)\1+$/
171
+ end
172
+
173
+ begin
174
+ require 'prime'
175
+ def next_prime(seed)
176
+ Prime.instance.find{|x| x > seed }
177
+ end
178
+ rescue LoadError
179
+ def next_prime(seed)
180
+ require 'mathn'
181
+ prime = Prime.new
182
+ while current = prime.succ
183
+ return current if current > seed
184
+ end
185
+ end
186
+ end
187
+
188
+ HASHWORDBITS = 32
189
+ # From gettext-0.12.1/gettext-runtime/intl/hash-string.h
190
+ # Defines the so called `hashpjw' function by P.J. Weinberger
191
+ # [see Aho/Sethi/Ullman, COMPILERS: Principles, Techniques and Tools,
192
+ # 1986, 1987 Bell Telephone Laboratories, Inc.]
193
+ def hash_string(str)
194
+ hval = 0
195
+ str.each_byte do |b|
196
+ break if b == '\0'
197
+ hval <<= 4
198
+ hval += b.to_i
199
+ g = hval & (0xf << (HASHWORDBITS - 4))
200
+ if (g != 0)
201
+ hval ^= g >> (HASHWORDBITS - 8)
202
+ hval ^= g
203
+ end
204
+ end
205
+ hval
206
+ end
207
+
208
+ #Save data as little endian format.
209
+ def save_to_stream(io)
210
+ # remove untranslated message
211
+ translated_messages = reject do |msgid, msgstr|
212
+ msgstr.nil?
213
+ end
214
+
215
+ size = translated_messages.size
216
+ header_size = 4 * 7
217
+ table_size = 4 * 2 * size
218
+
219
+ hash_table_size = next_prime((size * 4) / 3)
220
+ hash_table_size = 3 if hash_table_size <= 2
221
+ header = Header.new(
222
+ MAGIC_LITTLE_ENDIAN, # magic
223
+ 0, # revision
224
+ size, # nstrings
225
+ header_size, # orig_table_offset
226
+ header_size + table_size, # translated_table_offset
227
+ hash_table_size, # hash_table_size
228
+ header_size + table_size * 2 # hash_table_offset
229
+ )
230
+ io.write(header.to_a.pack('a4V*'))
231
+
232
+ ary = translated_messages.to_a
233
+ ary.sort!{|a, b| a[0] <=> b[0]} # sort by original string
234
+
235
+ pos = header.hash_table_size * 4 + header.hash_table_offset
236
+
237
+ orig_table_data = Array.new()
238
+ ary.each{|item, _|
239
+ orig_table_data.push(item.bytesize)
240
+ orig_table_data.push(pos)
241
+ pos += item.bytesize + 1 # +1 is <NUL>
242
+ }
243
+ io.write(orig_table_data.pack('V*'))
244
+
245
+ trans_table_data = Array.new()
246
+ ary.each{|_, item|
247
+ trans_table_data.push(item.bytesize)
248
+ trans_table_data.push(pos)
249
+ pos += item.bytesize + 1 # +1 is <NUL>
250
+ }
251
+ io.write(trans_table_data.pack('V*'))
252
+
253
+ hash_tab = Array.new(hash_table_size)
254
+ j = 0
255
+ ary[0...size].each {|key, _|
256
+ hash_val = hash_string(key)
257
+ idx = hash_val % hash_table_size
258
+ if hash_tab[idx] != nil
259
+ incr = 1 + (hash_val % (hash_table_size - 2))
260
+ begin
261
+ if (idx >= hash_table_size - incr)
262
+ idx -= hash_table_size - incr
263
+ else
264
+ idx += incr
265
+ end
266
+ end until (hash_tab[idx] == nil)
267
+ end
268
+ hash_tab[idx] = j + 1
269
+ j += 1
270
+ }
271
+ hash_tab.collect!{|i| i ? i : 0}
272
+
273
+ io.write(hash_tab.pack('V*'))
274
+
275
+ ary.each{|item, _| io.write(item); io.write("\0") }
276
+ ary.each{|_, item| io.write(item); io.write("\0") }
277
+
278
+ self
279
+ end
280
+
281
+ def load_from_file(filename)
282
+ @filename = filename
283
+ begin
284
+ File.open(filename, 'rb'){|f| load_from_stream(f)}
285
+ rescue => e
286
+ e.set_backtrace("File: #{@filename}")
287
+ raise e
288
+ end
289
+ end
290
+
291
+ def save_to_file(filename)
292
+ File.open(filename, 'wb'){|f| save_to_stream(f)}
293
+ end
294
+
295
+ def set_comment(msgid_or_sym, comment)
296
+ #Do nothing
297
+ end
298
+
299
+ def plural_as_proc
300
+ unless @plural_proc
301
+ @plural_proc = Proc.new{|n| eval(@plural)}
302
+ begin
303
+ @plural_proc.call(1)
304
+ rescue
305
+ @plural_proc = Proc.new{|n| 0}
306
+ end
307
+ end
308
+ @plural_proc
309
+ end
310
+
311
+ attr_accessor :little_endian, :path, :last_modified
312
+ attr_reader :charset, :nplurals, :plural
313
+
314
+ private
315
+ if "".respond_to?(:encode)
316
+ def convert_encoding(string, original_string)
317
+ return string if @output_charset.nil? or @charset.nil?
318
+
319
+ begin
320
+ string.encode(@output_charset, @charset)
321
+ rescue EncodingError
322
+ if $DEBUG
323
+ warn "@charset = ", @charset
324
+ warn "@output_charset = ", @output_charset
325
+ warn "msgid = ", original_string
326
+ warn "msgstr = ", string
327
+ end
328
+ string
329
+ end
330
+ end
331
+ else
332
+ require 'gettext/core_ext/iconv'
333
+ def convert_encoding(string, original_string)
334
+ begin
335
+ Iconv.conv(@output_charset, @charset, string)
336
+ rescue Iconv::Failure
337
+ if $DEBUG
338
+ warn "@charset = ", @charset
339
+ warn "@output_charset = ", @output_charset
340
+ warn "msgid = ", original_string
341
+ warn "msgstr = ", str
342
+ end
343
+ end
344
+ end
345
+ end
346
+
347
+ def generate_original_string(msgid, options)
348
+ string = ""
349
+
350
+ msgctxt = options.delete(:msgctxt)
351
+ msgid_plural = options.delete(:msgid_plural)
352
+
353
+ string << msgctxt << "\004" unless msgctxt.nil?
354
+ string << msgid
355
+ string << "\000" << msgid_plural unless msgid_plural.nil?
356
+ string
357
+ end
358
+ end
359
+ end
360
+
361
+ # Test
362
+
363
+ if $0 == __FILE__
364
+ if (ARGV.include? "-h") or (ARGV.include? "--help")
365
+ STDERR.puts("mo.rb [filename.mo ...]")
366
+ exit
367
+ end
368
+
369
+ ARGV.each{ |item|
370
+ mo = GetText::MO.open(item)
371
+ puts "------------------------------------------------------------------"
372
+ puts "charset = \"#{mo.charset}\""
373
+ puts "nplurals = \"#{mo.nplurals}\""
374
+ puts "plural = \"#{mo.plural}\""
375
+ puts "------------------------------------------------------------------"
376
+ mo.each do |key, value|
377
+ puts "original message = #{key.inspect}"
378
+ puts "translated message = #{value.inspect}"
379
+ puts "--------------------------------------------------------------------"
380
+ end
381
+ }
382
+ end
@@ -1,368 +1,26 @@
1
- # encoding: utf-8
2
-
3
- =begin
4
- mofile.rb - A simple class for operating GNU MO file.
5
-
6
- Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
7
- Copyright (C) 2003-2009 Masao Mutoh
8
- Copyright (C) 2002 Masahiro Sakai, Masao Mutoh
9
- Copyright (C) 2001 Masahiro Sakai
10
-
11
- Masahiro Sakai <s01397ms at sfc.keio.ac.jp>
12
- Masao Mutoh <mutomasa at gmail.com>
13
-
14
- You can redistribute this file and/or modify it under the same term
15
- of Ruby. License of Ruby is included with Ruby distribution in
16
- the file "README".
17
-
18
- =end
19
-
20
- require 'stringio'
21
-
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 2012 Haruka Yoshihara <yoshihara@clear-code.com>
4
+ #
5
+ # License: Ruby's or LGPL
6
+ #
7
+ # This library is free software: you can redistribute it and/or modify
8
+ # it under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation, either version 3 of the License, or
10
+ # (at your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ # GNU Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
19
+
20
+ require "gettext/runtime/mo"
21
+
22
+ # Just for backward compatibility.
22
23
  module GetText
23
- class MoFile < Hash
24
- class InvalidFormat < RuntimeError; end;
25
-
26
- attr_reader :filename
27
-
28
- Header = Struct.new(:magic,
29
- :revision,
30
- :nstrings,
31
- :orig_table_offset,
32
- :translated_table_offset,
33
- :hash_table_size,
34
- :hash_table_offset)
35
-
36
- # The following are only used in .mo files
37
- # with minor revision >= 1.
38
- class HeaderRev1 < Header
39
- attr_accessor :n_sysdep_segments,
40
- :sysdep_segments_offset,
41
- :n_sysdep_strings,
42
- :orig_sysdep_tab_offset,
43
- :trans_sysdep_tab_offset
44
- end
45
-
46
- MAGIC_BIG_ENDIAN = "\x95\x04\x12\xde"
47
- MAGIC_LITTLE_ENDIAN = "\xde\x12\x04\x95"
48
- if "".respond_to?(:force_encoding)
49
- MAGIC_BIG_ENDIAN.force_encoding("ASCII-8BIT")
50
- MAGIC_LITTLE_ENDIAN.force_encoding("ASCII-8BIT")
51
- end
52
-
53
- def self.open(arg = nil, output_charset = nil)
54
- result = self.new(output_charset)
55
- result.load(arg)
56
- end
57
-
58
- def initialize(output_charset = nil)
59
- @filename = nil
60
- @last_modified = nil
61
- @little_endian = true
62
- @output_charset = output_charset
63
- @plural_proc = nil
64
- super()
65
- end
66
-
67
- def update!
68
- if FileTest.exist?(@filename)
69
- st = File.stat(@filename)
70
- load(@filename) unless (@last_modified == [st.ctime, st.mtime])
71
- else
72
- warn "#{@filename} was lost." if $DEBUG
73
- clear
74
- end
75
- self
76
- end
77
-
78
- def load(arg)
79
- if arg.kind_of? String
80
- begin
81
- st = File.stat(arg)
82
- @last_modified = [st.ctime, st.mtime]
83
- rescue Exception
84
- end
85
- load_from_file(arg)
86
- else
87
- load_from_stream(arg)
88
- end
89
- @filename = arg
90
- self
91
- end
92
-
93
- def load_from_stream(io)
94
- magic = io.read(4)
95
- case magic
96
- when MAGIC_BIG_ENDIAN
97
- @little_endian = false
98
- when MAGIC_LITTLE_ENDIAN
99
- @little_endian = true
100
- else
101
- raise InvalidFormat.new(sprintf("Unknown signature %s", magic.dump))
102
- end
103
-
104
- endian_type6 = @little_endian ? 'V6' : 'N6'
105
- endian_type_astr = @little_endian ? 'V*' : 'N*'
106
-
107
- header = HeaderRev1.new(magic, *(io.read(4 * 6).unpack(endian_type6)))
108
-
109
- if header.revision == 1
110
- # FIXME: It doesn't support sysdep correctly.
111
- header.n_sysdep_segments = io.read(4).unpack(endian_type6)
112
- header.sysdep_segments_offset = io.read(4).unpack(endian_type6)
113
- header.n_sysdep_strings = io.read(4).unpack(endian_type6)
114
- header.orig_sysdep_tab_offset = io.read(4).unpack(endian_type6)
115
- header.trans_sysdep_tab_offset = io.read(4).unpack(endian_type6)
116
- elsif header.revision > 1
117
- raise InvalidFormat.new(sprintf("file format revision %d isn't supported", header.revision))
118
- end
119
- io.pos = header.orig_table_offset
120
- orig_table_data = io.read((4 * 2) * header.nstrings).unpack(endian_type_astr)
121
-
122
- io.pos = header.translated_table_offset
123
- trans_table_data = io.read((4 * 2) * header.nstrings).unpack(endian_type_astr)
124
-
125
- msgids = Array.new(header.nstrings)
126
- for i in 0...header.nstrings
127
- io.pos = orig_table_data[i * 2 + 1]
128
- msgids[i] = io.read(orig_table_data[i * 2 + 0])
129
- end
130
-
131
- clear
132
- for i in 0...header.nstrings
133
- io.pos = trans_table_data[i * 2 + 1]
134
- msgstr = io.read(trans_table_data[i * 2 + 0])
135
-
136
- msgid = msgids[i]
137
- if msgid.nil? || msgid.empty?
138
- if msgstr
139
- @charset = nil
140
- @nplurals = nil
141
- @plural = nil
142
- msgstr.each_line{|line|
143
- if /^Content-Type:/i =~ line and /charset=((?:\w|-)+)/i =~ line
144
- @charset = $1
145
- elsif /^Plural-Forms:\s*nplurals\s*\=\s*(\d*);\s*plural\s*\=\s*([^;]*)\n?/ =~ line
146
- @nplurals = $1
147
- @plural = $2
148
- end
149
- break if @charset and @nplurals
150
- }
151
- @nplurals = "1" unless @nplurals
152
- @plural = "0" unless @plural
153
- end
154
- else
155
- unless msgstr.nil?
156
- msgstr = convert_encoding(msgstr, msgid)
157
- end
158
- end
159
- self[convert_encoding(msgid, msgid)] = msgstr.freeze
160
- end
161
- self
162
- end
163
-
164
- def prime?(number)
165
- ('1' * number) !~ /^1?$|^(11+?)\1+$/
166
- end
167
-
168
- begin
169
- require 'prime'
170
- def next_prime(seed)
171
- Prime.instance.find{|x| x > seed }
172
- end
173
- rescue LoadError
174
- def next_prime(seed)
175
- require 'mathn'
176
- prime = Prime.new
177
- while current = prime.succ
178
- return current if current > seed
179
- end
180
- end
181
- end
182
-
183
- HASHWORDBITS = 32
184
- # From gettext-0.12.1/gettext-runtime/intl/hash-string.h
185
- # Defines the so called `hashpjw' function by P.J. Weinberger
186
- # [see Aho/Sethi/Ullman, COMPILERS: Principles, Techniques and Tools,
187
- # 1986, 1987 Bell Telephone Laboratories, Inc.]
188
- def hash_string(str)
189
- hval = 0
190
- str.each_byte do |b|
191
- break if b == '\0'
192
- hval <<= 4
193
- hval += b.to_i
194
- g = hval & (0xf << (HASHWORDBITS - 4))
195
- if (g != 0)
196
- hval ^= g >> (HASHWORDBITS - 8)
197
- hval ^= g
198
- end
199
- end
200
- hval
201
- end
202
-
203
- #Save data as little endian format.
204
- def save_to_stream(io)
205
- # remove untranslated message
206
- translated_messages = reject do |msgid, msgstr|
207
- msgstr.nil?
208
- end
209
-
210
- size = translated_messages.size
211
- header_size = 4 * 7
212
- table_size = 4 * 2 * size
213
-
214
- hash_table_size = next_prime((size * 4) / 3)
215
- hash_table_size = 3 if hash_table_size <= 2
216
- header = Header.new(
217
- MAGIC_LITTLE_ENDIAN, # magic
218
- 0, # revision
219
- size, # nstrings
220
- header_size, # orig_table_offset
221
- header_size + table_size, # translated_table_offset
222
- hash_table_size, # hash_table_size
223
- header_size + table_size * 2 # hash_table_offset
224
- )
225
- io.write(header.to_a.pack('a4V*'))
226
-
227
- ary = translated_messages.to_a
228
- ary.sort!{|a, b| a[0] <=> b[0]} # sort by original string
229
-
230
- pos = header.hash_table_size * 4 + header.hash_table_offset
231
-
232
- orig_table_data = Array.new()
233
- ary.each{|item, _|
234
- orig_table_data.push(item.bytesize)
235
- orig_table_data.push(pos)
236
- pos += item.bytesize + 1 # +1 is <NUL>
237
- }
238
- io.write(orig_table_data.pack('V*'))
239
-
240
- trans_table_data = Array.new()
241
- ary.each{|_, item|
242
- trans_table_data.push(item.bytesize)
243
- trans_table_data.push(pos)
244
- pos += item.bytesize + 1 # +1 is <NUL>
245
- }
246
- io.write(trans_table_data.pack('V*'))
247
-
248
- hash_tab = Array.new(hash_table_size)
249
- j = 0
250
- ary[0...size].each {|key, _|
251
- hash_val = hash_string(key)
252
- idx = hash_val % hash_table_size
253
- if hash_tab[idx] != nil
254
- incr = 1 + (hash_val % (hash_table_size - 2))
255
- begin
256
- if (idx >= hash_table_size - incr)
257
- idx -= hash_table_size - incr
258
- else
259
- idx += incr
260
- end
261
- end until (hash_tab[idx] == nil)
262
- end
263
- hash_tab[idx] = j + 1
264
- j += 1
265
- }
266
- hash_tab.collect!{|i| i ? i : 0}
267
-
268
- io.write(hash_tab.pack('V*'))
269
-
270
- ary.each{|item, _| io.write(item); io.write("\0") }
271
- ary.each{|_, item| io.write(item); io.write("\0") }
272
-
273
- self
274
- end
275
-
276
- def load_from_file(filename)
277
- @filename = filename
278
- begin
279
- File.open(filename, 'rb'){|f| load_from_stream(f)}
280
- rescue => e
281
- e.set_backtrace("File: #{@filename}")
282
- raise e
283
- end
284
- end
285
-
286
- def save_to_file(filename)
287
- File.open(filename, 'wb'){|f| save_to_stream(f)}
288
- end
289
-
290
- def set_comment(msgid_or_sym, comment)
291
- #Do nothing
292
- end
293
-
294
- def plural_as_proc
295
- unless @plural_proc
296
- @plural_proc = Proc.new{|n| eval(@plural)}
297
- begin
298
- @plural_proc.call(1)
299
- rescue
300
- @plural_proc = Proc.new{|n| 0}
301
- end
302
- end
303
- @plural_proc
304
- end
305
-
306
- attr_accessor :little_endian, :path, :last_modified
307
- attr_reader :charset, :nplurals, :plural
308
-
309
- private
310
- if "".respond_to?(:encode)
311
- def convert_encoding(string, original_string)
312
- return string if @output_charset.nil? or @charset.nil?
313
-
314
- begin
315
- string.encode(@output_charset, @charset)
316
- rescue EncodingError
317
- if $DEBUG
318
- warn "@charset = ", @charset
319
- warn "@output_charset = ", @output_charset
320
- warn "msgid = ", original_string
321
- warn "msgstr = ", string
322
- end
323
- string
324
- end
325
- end
326
- else
327
- require 'gettext/core_ext/iconv'
328
- def convert_encoding(string, original_string)
329
- begin
330
- Iconv.conv(@output_charset, @charset, string)
331
- rescue Iconv::Failure
332
- if $DEBUG
333
- warn "@charset = ", @charset
334
- warn "@output_charset = ", @output_charset
335
- warn "msgid = ", original_string
336
- warn "msgstr = ", str
337
- end
338
- end
339
- end
340
- end
341
- end
342
-
343
- # Just for backward compatibility.
344
- MOFile = MoFile
345
- end
346
-
347
- # Test
348
-
349
- if $0 == __FILE__
350
- if (ARGV.include? "-h") or (ARGV.include? "--help")
351
- STDERR.puts("mo.rb [filename.mo ...]")
352
- exit
353
- end
354
-
355
- ARGV.each{ |item|
356
- mo = GetText::MoFile.open(item)
357
- puts "------------------------------------------------------------------"
358
- puts "charset = \"#{mo.charset}\""
359
- puts "nplurals = \"#{mo.nplurals}\""
360
- puts "plural = \"#{mo.plural}\""
361
- puts "------------------------------------------------------------------"
362
- mo.each do |key, value|
363
- puts "original message = #{key.inspect}"
364
- puts "translated message = #{value.inspect}"
365
- puts "--------------------------------------------------------------------"
366
- end
367
- }
24
+ MoFile = MO
25
+ MOFile = MO
368
26
  end