gettext 2.3.3 → 2.3.4

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