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
@@ -14,7 +14,7 @@
14
14
  =end
15
15
 
16
16
  require 'gettext/core_ext/string'
17
- require 'gettext/runtime/mofile'
17
+ require 'gettext/runtime/mo'
18
18
  require 'gettext/runtime/locale_path'
19
19
 
20
20
  module GetText
@@ -72,26 +72,26 @@ module GetText
72
72
 
73
73
  lang_key = lang.to_s
74
74
 
75
- mofile = nil
75
+ mo = nil
76
76
  if self.class.cached?
77
- mofile = @mofiles[lang_key]
77
+ mo = @mofiles[lang_key]
78
78
  end
79
- unless mofile
80
- mofile = load_mo(lang)
79
+ unless mo
80
+ mo = load_mo(lang)
81
81
  end
82
82
 
83
- if (! mofile) or (mofile ==:empty)
83
+ if (! mo) or (mo ==:empty)
84
84
  return nil
85
85
  end
86
86
 
87
- return mofile[msgid] if mofile.has_key?(msgid)
87
+ return mo[msgid] if mo.has_key?(msgid)
88
88
 
89
89
  ret = nil
90
90
  if msgid.include?("\000")
91
91
  # Check "aaa\000bbb" and show warning but return the singular part.
92
92
  msgid_single = msgid.split("\000")[0]
93
93
  msgid_single_prefix_re = /^#{Regexp.quote(msgid_single)}\000/
94
- mofile.each do |key, val|
94
+ mo.each do |key, val|
95
95
  if msgid_single_prefix_re =~ key
96
96
  # Usually, this is not caused to make po-files from rgettext.
97
97
  separated_msgid = msgid.gsub(/\000/, '", "')
@@ -106,7 +106,7 @@ module GetText
106
106
  end
107
107
  else
108
108
  msgid_prefix_re = /^#{Regexp.quote(msgid)}\000/
109
- mofile.each do |key, val|
109
+ mo.each do |key, val|
110
110
  if msgid_prefix_re =~ key
111
111
  ret = val.split("\000")[0]
112
112
  break
@@ -132,8 +132,8 @@ module GetText
132
132
  ret = nil
133
133
  elsif msg.include?("\000")
134
134
  # [[msgstr[0], msgstr[1], msgstr[2],...], cond]
135
- mofile = @mofiles[lang.to_posix.to_s]
136
- cond = (mofile and mofile != :empty) ? mofile.plural_as_proc : DEFAULT_PLURAL_CALC
135
+ mo = @mofiles[lang.to_posix.to_s]
136
+ cond = (mo and mo != :empty) ? mo.plural_as_proc : DEFAULT_PLURAL_CALC
137
137
  ret = [msg.split("\000"), cond]
138
138
  else
139
139
  ret = [[msg], DEFAULT_SINGLE_CALC]
@@ -160,21 +160,21 @@ module GetText
160
160
  lang = lang.to_posix unless lang.kind_of? Locale::Tag::Posix
161
161
  lang_key = lang.to_s
162
162
 
163
- mofile = @mofiles[lang_key]
164
- if mofile
165
- if mofile == :empty
163
+ mo = @mofiles[lang_key]
164
+ if mo
165
+ if mo == :empty
166
166
  return :empty
167
167
  elsif ! self.class.cached?
168
- mofile.update!
168
+ mo.update!
169
169
  end
170
- return mofile
170
+ return mo
171
171
  end
172
172
 
173
173
  path = @locale_path.current_path(lang)
174
174
 
175
175
  if path
176
176
  charset = @output_charset || lang.charset || Locale.charset || "UTF-8"
177
- @mofiles[lang_key] = MoFile.open(path, charset)
177
+ @mofiles[lang_key] = MO.open(path, charset)
178
178
  else
179
179
  @mofiles[lang_key] = :empty
180
180
  end
data/lib/gettext/tools.rb CHANGED
@@ -29,7 +29,7 @@ require 'gettext/tools/xgettext'
29
29
  require 'gettext/tools/msgfmt'
30
30
  require 'gettext/tools/msginit'
31
31
  require 'gettext/tools/msgmerge'
32
- require 'gettext/runtime/mofile'
32
+ require 'gettext/runtime/mo'
33
33
  require 'fileutils'
34
34
 
35
35
  module GetText
@@ -51,8 +51,8 @@ module GetText
51
51
  def run(*options) # :nodoc:
52
52
  initialize_arguments(*options)
53
53
 
54
- parser = PoParser.new
55
- data = MoFile.new
54
+ parser = POParser.new
55
+ data = MO.new
56
56
 
57
57
  parser.parse_file(@input_file, data)
58
58
  data.save_to_file(@output_file)
@@ -66,14 +66,14 @@ module GetText
66
66
  parse_arguments(*arguments)
67
67
  validate
68
68
 
69
- parser = PoParser.new
69
+ parser = POParser.new
70
70
  parser.ignore_fuzzy = false
71
71
  pot = parser.parse_file(@input_file,
72
- GetText::Tools::MsgMerge::PoData.new)
72
+ GetText::PO.new)
73
73
  po = replace_pot_header(pot)
74
74
 
75
75
  File.open(@output_file, "w") do |f|
76
- f.puts(po.generate_po)
76
+ f.puts(po.to_s)
77
77
  end
78
78
  end
79
79
 
@@ -169,15 +169,16 @@ module GetText
169
169
  end
170
170
 
171
171
  def replace_pot_header(pot) #:nodoc:
172
- @entry = pot[""]
173
- @comment = pot.comment("")
172
+ @entry = pot[""].msgstr
173
+ @comment = pot[""].translator_comment
174
174
  @translator = translator_info
175
175
 
176
176
  replace_entry
177
177
  replace_comment
178
178
 
179
- pot[""] = @entry.chomp
180
- pot.set_comment("", @comment)
179
+ pot[""] = @entry
180
+ pot[""].translator_comment = @comment
181
+ pot[""].flag = pot[""].flag.gsub(/\Afuzzy\z/, "")
181
182
  pot
182
183
  end
183
184
 
@@ -239,11 +240,11 @@ module GetText
239
240
  replace_description
240
241
  replace_first_author
241
242
  replace_copyright_year
242
- @comment = @comment.gsub(/#, fuzzy/, "")
243
+ @comment = @comment.gsub(/^fuzzy$/, "")
243
244
  end
244
245
 
245
246
  EMAIL = "EMAIL@ADDRESS"
246
- FIRST_AUTHOR_KEY = /^(\s*#\s*) FIRST AUTHOR <#{EMAIL}>, (\d+\.)$/
247
+ FIRST_AUTHOR_KEY = /^FIRST AUTHOR <#{EMAIL}>, (\d+\.)$/
247
248
 
248
249
  def replace_last_translator #:nodoc:
249
250
  unless @translator.nil?
@@ -273,9 +274,9 @@ module GetText
273
274
  def replace_plural_forms #:nodoc:
274
275
  plural_entry = plural_forms(@language)
275
276
  if PLURAL_FORMS =~ @entry
276
- @entry = @entry.gsub(PLURAL_FORMS, "\\1 #{plural_entry}")
277
+ @entry = @entry.gsub(PLURAL_FORMS, "\\1 #{plural_entry}\n")
277
278
  else
278
- @entry << "Plural-Forms: #{plural_entry}"
279
+ @entry << "Plural-Forms: #{plural_entry}\n"
279
280
  end
280
281
  end
281
282
 
@@ -329,7 +330,7 @@ module GetText
329
330
  "nplurals=#{nplural}; plural=#{plural_expression};"
330
331
  end
331
332
 
332
- DESCRIPTION_TITLE = /^(\s*#\s*) SOME DESCRIPTIVE TITLE\.$/
333
+ DESCRIPTION_TITLE = /^SOME DESCRIPTIVE TITLE\.$/
333
334
 
334
335
  def replace_description #:nodoc:
335
336
  language_name = Locale::Info.get_language(@language).name
@@ -342,18 +343,18 @@ module GetText
342
343
  @comment = @comment.gsub(DESCRIPTION_TITLE, "\\1 #{description}")
343
344
  end
344
345
 
345
- YEAR_KEY = /^(\s*#\s* FIRST AUTHOR <#{EMAIL}>,) YEAR\.$/
346
+ YEAR_KEY = /^(FIRST AUTHOR <#{EMAIL}>,) YEAR\.$/
346
347
  LAST_TRANSLATOR_KEY = /^(Last-Translator:) FULL NAME <#{EMAIL}>$/
347
348
 
348
349
  def replace_first_author #:nodoc:
349
350
  @comment = @comment.gsub(YEAR_KEY, "\\1 #{year}.")
350
351
  unless @translator.nil?
351
352
  @comment = @comment.gsub(FIRST_AUTHOR_KEY,
352
- "\\1 #{@translator}, \\2")
353
+ "#{@translator}, \\1")
353
354
  end
354
355
  end
355
356
 
356
- COPYRIGHT_KEY = /(\s*#\s* Copyright \(C\)) YEAR (THE PACKAGE'S COPYRIGHT HOLDER)$/
357
+ COPYRIGHT_KEY = /^(Copyright \(C\)) YEAR (THE PACKAGE'S COPYRIGHT HOLDER)$/
357
358
  def replace_copyright_year #:nodoc:
358
359
  @comment = @comment.gsub(COPYRIGHT_KEY, "\\1 #{year} \\2")
359
360
  end
@@ -20,64 +20,110 @@
20
20
  # along with this program. If not, see <http://www.gnu.org/licenses/>.
21
21
 
22
22
  require "optparse"
23
+ require "levenshtein"
23
24
  require "gettext"
24
25
  require "gettext/tools/poparser"
25
- require "gettext/tools/pomessage"
26
-
27
- # TODO: MsgMerge should use PoMessage to generate PO content.
26
+ require "gettext/tools/po"
28
27
 
29
28
  module GetText
30
29
  module Tools
31
30
  class MsgMerge
32
31
  class PoData #:nodoc:
33
32
 
34
- attr_reader :msgids
33
+ attr_reader :po
35
34
 
36
35
  def initialize
37
- @msgid2msgstr = {}
38
- @msgid2comment = {}
39
- @msgids = []
36
+ @po = PO.new
40
37
  end
41
38
 
42
- def set_comment(msgid_or_sym, comment)
43
- @msgid2comment[msgid_or_sym] = comment
39
+ def set_comment(msgid, comments, msgctxt=nil)
40
+ entry = generate_entry(msgid)
41
+
42
+ if msgid == :last
43
+ entry.comment = comments
44
+ return
45
+ end
46
+
47
+ comments.each_line do |_line|
48
+ line = _line.chomp
49
+ entry = parse_comment(line, entry)
50
+ end
44
51
  end
45
52
 
46
53
  def msgstr(msgid)
47
- @msgid2msgstr[msgid]
54
+ self[msgid]
48
55
  end
49
56
 
50
57
  def comment(msgid)
51
- @msgid2comment[msgid]
58
+ msgctxt, msgid, _ = split_msgid(msgid)
59
+ id = [msgctxt, msgid]
60
+ entry = @po[*id]
61
+ return nil if entry.nil?
62
+
63
+ formatted_comments = entry.format_translator_comment
64
+ formatted_comments << entry.format_extracted_comment
65
+ formatted_comments << entry.format_reference_comment
66
+ formatted_comments << entry.format_flag_comment
67
+ formatted_comments << entry.format_previous_comment
68
+
69
+ unless entry.comment.nil?
70
+ formatted_comments = entry.format_comment("#", entry.comment)
71
+ end
72
+
73
+ formatted_comments.chomp
52
74
  end
53
75
 
54
76
  def [](msgid)
55
- @msgid2msgstr[msgid]
77
+ msgctxt, msgid, _ = split_msgid(msgid)
78
+ @po[msgctxt, msgid].msgstr
56
79
  end
57
80
 
58
- def []=(msgid, msgstr)
59
- # Retain the order
60
- unless @msgid2msgstr.has_key?(msgid)
61
- @msgids << msgid
81
+ def []=(msgid, value)
82
+ msgctxt, msgid, msgid_plural = split_msgid(msgid)
83
+ id = [msgctxt, msgid]
84
+
85
+ if value.instance_of?(POEntry)
86
+ @po[*id] = value
87
+ return value
62
88
  end
63
89
 
64
- @msgid2msgstr[msgid] = msgstr
90
+ msgstr = value
91
+ if @po.has_key?(*id)
92
+ @po[*id] = msgstr
93
+ @po[*id].msgctxt = msgctxt
94
+ @po[*id].msgid_plural = msgid_plural
95
+ else
96
+ type = detect_entry_type(msgctxt, msgid_plural)
97
+ entry = POEntry.new(type)
98
+ entry.msgctxt = msgctxt
99
+ entry.msgid = msgid
100
+ entry.msgid_plural = msgid_plural
101
+ entry.msgstr = msgstr
102
+ @po[*id] = entry
103
+ entry
104
+ end
65
105
  end
66
106
 
67
107
  def each_msgid
68
- msgids = @msgids.delete_if do |msgid|
69
- msgid.kind_of?(Symbol) or msgid.empty?
108
+ msgids.each do |id|
109
+ next if id.kind_of?(Symbol) or id.empty?
110
+ yield(id)
70
111
  end
112
+ end
71
113
 
72
- msgids.each do |msgid|
73
- yield(msgid)
114
+ def msgids
115
+ @po.collect do |entry|
116
+ msgctxt = entry.msgctxt
117
+ msgid = entry.msgid
118
+ generate_original_string(msgctxt, msgid)
74
119
  end
75
120
  end
76
121
 
77
122
  def msgid?(msgid)
78
123
  return false if msgid.kind_of?(Symbol)
79
124
  return true if msgid.empty?
80
- @msgid2msgstr.has_key?(msgid)
125
+ msgctxt, msgid, _ = split_msgid(msgid)
126
+ @po.has_key?(msgctxt, msgid)
81
127
  end
82
128
 
83
129
  # Is it necessary to implement this method?
@@ -86,9 +132,9 @@ module GetText
86
132
  end
87
133
 
88
134
  def nplurals
89
- return 0 if @msgid2msgstr[""].nil?
135
+ return 0 if @po[""].msgstr.nil?
90
136
 
91
- if /\s*nplurals\s*=\s*(\d+)/ =~ @msgid2msgstr[""]
137
+ if /\s*nplurals\s*=\s*(\d+)/ =~ @po[""].msgstr
92
138
  return $1.to_i
93
139
  else
94
140
  return 0
@@ -96,81 +142,12 @@ module GetText
96
142
  end
97
143
 
98
144
  def generate_po
99
- str = ""
100
- str << generate_po_header
101
- str << "\n"
102
-
103
- po_entries = []
104
- self.each_msgid do |id|
105
- po_entries << self.generate_po_entry(id)
106
- end
107
-
108
- unless @msgid2comment[:last].nil?
109
- po_entries << "#{@msgid2comment[:last]}\n"
110
- end
111
-
112
- str << po_entries.join("\n")
113
- str
114
- end
115
-
116
- def generate_po_header
117
- str = ""
118
-
119
- str << @msgid2comment[""].strip << "\n"
120
- str << 'msgid ""' << "\n"
121
- str << 'msgstr ""' << "\n"
122
- msgstr = @msgid2msgstr[""].gsub(/"/, '\"').gsub(/\r/, "")
123
- msgstr = msgstr.gsub(/^(.*)$/, '"\1\n"')
124
- str << msgstr.chomp
125
- str << "\n"
126
-
127
- str
145
+ @po.to_s
128
146
  end
129
147
 
130
148
  def generate_po_entry(msgid)
131
- str = ""
132
- str << @msgid2comment[msgid]
133
- if str[-1] != "\n"[0]
134
- str << "\n"
135
- end
136
-
137
- id = msgid.gsub(/\r/, "")
138
- msgstr = @msgid2msgstr[msgid]
139
- if msgstr.nil?
140
- msgstr = ""
141
- else
142
- msgstr = msgstr.gsub(/\r/, "")
143
- end
144
-
145
- if id.include?("\004")
146
- ids = id.split(/\004/)
147
- context = ids[0]
148
- id = ids[1]
149
- str << "msgctxt " << __conv(context) << "\n"
150
- end
151
-
152
- if id.include?("\000")
153
- ids = id.split(/\000/)
154
- str << "msgid " << __conv(ids[0]) << "\n"
155
- ids[1..-1].each do |single_id|
156
- str << "msgid_plural " << __conv(single_id) << "\n"
157
- end
158
-
159
- if msgstr.empty?
160
- nplurals.times do |id_count|
161
- str << "msgstr[#{id_count}] " << '""' << "\n"
162
- end
163
- else
164
- msgstr.split("\000", -1).each_with_index do |m, n|
165
- str << "msgstr[#{n}] " << __conv(m) << "\n"
166
- end
167
- end
168
- else
169
- str << "msgid " << __conv(id) << "\n"
170
- str << "msgstr " << __conv(msgstr) << "\n"
171
- end
172
-
173
- str
149
+ msgctxt, msgid, _ = split_msgid(msgid)
150
+ @po[msgctxt, msgid].to_s
174
151
  end
175
152
 
176
153
  def __conv(str)
@@ -189,209 +166,235 @@ module GetText
189
166
  end
190
167
 
191
168
  def escape(string)
192
- PoMessage.escape(string)
169
+ POEntry.escape(string)
170
+ end
171
+
172
+ private
173
+ def split_msgid(msgid)
174
+ return [nil, msgid, nil] if msgid == :last
175
+ return [nil, "", nil] if msgid.empty?
176
+ msgctxt, msgid = msgid.split("\004", 2)
177
+ if msgid.nil?
178
+ msgid = msgctxt
179
+ msgctxt = nil
180
+ end
181
+ msgid, msgid_plural = msgid.split("\000", 2)
182
+ [msgctxt, msgid, msgid_plural]
183
+ end
184
+
185
+ def generate_original_string(msgctxt, msgid)
186
+ return msgid if msgid == :last
187
+ original_string = ""
188
+ msgid_plural = @po[msgctxt, msgid].msgid_plural
189
+ original_string << "#{msgctxt}\004" unless msgctxt.nil?
190
+ original_string << msgid
191
+ original_string << "\000#{msgid_plural}" unless msgid_plural.nil?
192
+ original_string
193
+ end
194
+
195
+ def detect_entry_type(msgctxt, msgid_plural)
196
+ if msgctxt.nil?
197
+ if msgid_plural.nil?
198
+ :normal
199
+ else
200
+ :plural
201
+ end
202
+ else
203
+ if msgid_plural.nil?
204
+ :msgctxt
205
+ else
206
+ :msgctxt_plural
207
+ end
208
+ end
209
+ end
210
+
211
+ def generate_entry(msgid)
212
+ msgctxt, msgid, _ = split_msgid(msgid)
213
+ id = [msgctxt, msgid]
214
+ @po[*id] = nil unless @po.has_key?(*id)
215
+ entry = @po[*id]
216
+
217
+ entry.translator_comment = ""
218
+ entry.extracted_comment = ""
219
+ entry.references = []
220
+ entry.flag = ""
221
+ entry.previous = ""
222
+ entry
223
+ end
224
+
225
+ def parse_comment(line, entry)
226
+ if line == "#"
227
+ entry.translator_comment << ""
228
+ elsif /\A(#.)\s*(.*)\z/ =~ line
229
+ mark = $1
230
+ content = $2
231
+ case mark
232
+ when POParser::TRANSLATOR_COMMENT_MARK
233
+ entry.translator_comment << "#{content}\n"
234
+ when POParser::EXTRACTED_COMMENT_MARK
235
+ entry.extracted_comment << "#{content}\n"
236
+ when POParser::REFERENCE_COMMENT_MARK
237
+ entry.references << content
238
+ when POParser::FLAG_MARK
239
+ entry.flag << "#{content}\n"
240
+ when POParser::PREVIOUS_MSGID_COMMENT_MARK
241
+ entry.previous << "#{content}\n"
242
+ else
243
+ entry.comment << line
244
+ end
245
+ end
246
+ entry
193
247
  end
194
248
  end
195
249
 
196
250
  class Merger #:nodoc:
197
- # From GNU gettext source.
198
- #
199
251
  # Merge the reference with the definition: take the #. and
200
252
  # #: comments from the reference, take the # comments from
201
253
  # the definition, take the msgstr from the definition. Add
202
254
  # this merged entry to the output message list.
203
255
 
204
- DOT_COMMENT_RE = /\A#\./
205
- SEMICOLON_COMMENT_RE = /\A#\:/
206
- FUZZY_RE = /\A#\,/
207
- NOT_SPECIAL_COMMENT_RE = /\A#([^:.,]|\z)/
208
-
209
- CRLF_RE = /\r?\n/
210
256
  POT_DATE_EXTRACT_RE = /POT-Creation-Date:\s*(.*)?\s*$/
211
257
  POT_DATE_RE = /POT-Creation-Date:.*?$/
212
258
 
213
259
  def merge(definition, reference)
214
- definition.each_msgid do |msgid|
215
- msgstr = definition[msgid] || ""
216
- definition[msgid] = msgstr
217
- end
260
+ result = GetText::PO.new
218
261
 
219
- reference.each_msgid do |msgid|
220
- msgstr = reference[msgid] || ""
221
- reference[msgid] = msgstr
222
- end
262
+ reference.each do |entry|
263
+ msgid = entry.msgid
264
+ msgctxt = entry.msgctxt
265
+ id = [msgctxt, msgid]
223
266
 
224
- # deep copy
225
- result = Marshal.load( Marshal.dump(reference) )
226
-
227
- used = []
228
- merge_header(result, definition)
229
-
230
- result.each_msgid do |msgid|
231
- if definition.msgid?(msgid)
232
- used << msgid
233
- merge_message(msgid, result, msgid, definition)
234
- elsif other_msgid = definition.search_msgid_fuzzy(msgid, used)
235
- used << other_msgid
236
- merge_fuzzy_message(msgid, result, other_msgid, definition)
237
- elsif msgid.index("\000") and (reference.msgstr(msgid).gsub("\000", "").empty?)
238
- # plural
239
- result[msgid] = ([""] * definition.nplurals).join("\000")
240
- else
241
- change_reference_comment(msgid, result)
267
+ if definition.has_key?(*id)
268
+ result[*id] = merge_entry(definition[*id], entry)
269
+ next
242
270
  end
243
- end
244
271
 
245
- ###################################################################
246
- # msgids which are not used in reference are handled as obsolete. #
247
- ###################################################################
248
- last_comment = result.comment(:last) || ""
249
- definition.each_msgid do |msgid|
250
- unless used.include?(msgid)
251
- last_comment << "\n"
252
- last_comment << definition.generate_po_entry(msgid).strip.gsub(/^/, "#. ")
253
- last_comment << "\n"
272
+ if msgctxt.nil?
273
+ same_msgid_entry = find_by_msgid(definition, msgid)
274
+ if not same_msgid_entry.nil? and not same_msgid_entry.msgctxt.nil?
275
+ result[nil, msgid] = merge_fuzzy_entry(same_msgid_entry, entry)
276
+ next
277
+ end
254
278
  end
279
+
280
+ fuzzy_entry = find_fuzzy_entry(definition, msgid, msgctxt)
281
+ unless fuzzy_entry.nil?
282
+ result[*id] = merge_fuzzy_entry(fuzzy_entry, entry)
283
+ next
284
+ end
285
+
286
+ result[*id] = entry
255
287
  end
256
- result.set_comment(:last, last_comment) unless last_comment.empty?
257
288
 
289
+ add_obsolete_entry(result, definition)
258
290
  result
259
291
  end
260
292
 
261
- def merge_message(msgid, target, def_msgid, definition)
262
- merge_comment(msgid, target, def_msgid, definition)
293
+ def merge_entry(definition_entry, reference_entry)
294
+ if definition_entry.msgid.empty? and definition_entry.msgctxt.nil?
295
+ new_header = merge_header(definition_entry, reference_entry)
296
+ return new_header
297
+ end
263
298
 
264
- ############################################
265
- # check mismatch of msgid and msgid_plural #
266
- ############################################
267
- def_msgstr = definition[def_msgid]
268
- if msgid.index("\000")
269
- if def_msgstr.index("\000")
270
- # OK
271
- target[msgid] = def_msgstr
272
- else
273
- # NG
274
- strings = []
275
- definition.nplurals.times do
276
- strings << def_msgstr
277
- end
278
- target[msgid] = strings.join("\000")
279
- end
280
- else
281
- if def_msgstr.index("\000")
282
- # NG
283
- target[msgid] = def_msgstr.split("\000")[0]
284
- else
285
- # OK
286
- target[msgid] = def_msgstr
287
- end
299
+ if definition_entry.flag == "fuzzy"
300
+ entry = definition_entry
301
+ entry.flag = "fuzzy"
302
+ return entry
288
303
  end
289
- end
290
304
 
291
- # for the future
292
- def merge_fuzzy_message(msgid, target, def_msgid, definition)
293
- merge_message(msgid, target, def_msgid, definition)
294
- end
305
+ entry = reference_entry
306
+ entry.translator_comment = definition_entry.translator_comment
307
+ entry.previous = nil
295
308
 
296
- def merge_comment(msgid, target, def_msgid, definition)
297
- ref_comment = target.comment(msgid)
298
- def_comment = definition.comment(def_msgid)
309
+ unless definition_entry.msgid_plural == reference_entry.msgid_plural
310
+ entry.flag = "fuzzy"
311
+ end
299
312
 
300
- normal_comment = []
301
- dot_comment = []
302
- semi_comment = []
303
- is_fuzzy = false
313
+ entry.msgstr = definition_entry.msgstr
314
+ entry
315
+ end
304
316
 
305
- def_comment.split(CRLF_RE).each do |l|
306
- if NOT_SPECIAL_COMMENT_RE =~ l
307
- normal_comment << l
308
- end
317
+ def merge_header(old_header, new_header)
318
+ header = old_header
319
+ if POT_DATE_EXTRACT_RE =~ new_header.msgstr
320
+ create_date = $1
321
+ pot_creation_date = "POT-Creation-Date: #{create_date}"
322
+ header.msgstr = header.msgstr.gsub(POT_DATE_RE, pot_creation_date)
309
323
  end
324
+ header.flag = nil
325
+ header
326
+ end
310
327
 
311
- ref_comment.split(CRLF_RE).each do |l|
312
- if DOT_COMMENT_RE =~ l
313
- dot_comment << l
314
- elsif SEMICOLON_COMMENT_RE =~ l
315
- semi_comment << l
316
- elsif FUZZY_RE =~ l
317
- is_fuzzy = true if msgid != ""
318
- end
328
+ def find_by_msgid(entries, msgid)
329
+ same_msgid_entries = entries.find_all do |entry|
330
+ entry.msgid == msgid
319
331
  end
332
+ same_msgid_entries = same_msgid_entries.sort_by do |entry|
333
+ entry.msgctxt
334
+ end
335
+ same_msgid_entries.first
336
+ end
320
337
 
321
- str = format_comment(normal_comment, dot_comment, semi_comment, is_fuzzy)
322
- target.set_comment(msgid, str)
338
+ def merge_fuzzy_entry(fuzzy_entry, entry)
339
+ merged_entry = merge_entry(fuzzy_entry, entry)
340
+ merged_entry.flag = "fuzzy"
341
+ merged_entry
323
342
  end
324
343
 
325
- def change_reference_comment(msgid, podata)
326
- normal_comment = []
327
- dot_comment = []
328
- semi_comment = []
329
- is_fuzzy = false
344
+ MAX_FUZZY_DISTANCE = 0.5 # XXX: make sure that its value is proper.
330
345
 
331
- podata.comment(msgid).split(CRLF_RE).each do |l|
332
- if DOT_COMMENT_RE =~ l
333
- dot_comment << l
334
- elsif SEMICOLON_COMMENT_RE =~ l
335
- semi_comment << l
336
- elsif FUZZY_RE =~ l
337
- is_fuzzy = true
338
- else
339
- normal_comment << l
346
+ def find_fuzzy_entry(definition, msgid, msgctxt)
347
+ min_distance_entry = nil
348
+ min_distance = MAX_FUZZY_DISTANCE
349
+
350
+ same_msgctxt_entries = definition.find_all do |entry|
351
+ entry.msgctxt == msgctxt
352
+ end
353
+ same_msgctxt_entries.each do |entry|
354
+ distance = Levenshtein.normalized_distance(entry.msgid, msgid)
355
+ if min_distance > distance
356
+ min_distance = distance
357
+ min_distance_entry = entry
340
358
  end
341
359
  end
342
360
 
343
- str = format_comment(normal_comment, dot_comment, semi_comment, is_fuzzy)
344
- podata.set_comment(msgid, str)
361
+ min_distance_entry
345
362
  end
346
363
 
347
- def format_comment(normal_comment, dot_comment, semi_comment, is_fuzzy)
348
- str = ""
349
-
350
- str << normal_comment.join("\n").gsub(/^#(\s*)/) do |sss|
351
- if $1 == ""
352
- "# "
353
- else
354
- sss
355
- end
356
- end
357
- if normal_comment.size > 0
358
- str << "\n"
364
+ def add_obsolete_entry(result, definition)
365
+ obsolete_entry = generate_obsolete_entry(result, definition)
366
+ unless obsolete_entry.nil?
367
+ result[:last] = obsolete_entry
359
368
  end
369
+ result
370
+ end
360
371
 
361
- str << dot_comment.join("\n").gsub(/^#.(\s*)/) do |sss|
362
- if $1 == ""
363
- "#. "
364
- else
365
- sss
366
- end
367
- end
368
- if dot_comment.size > 0
369
- str << "\n"
370
- end
372
+ def generate_obsolete_entry(result, definition)
373
+ obsolete_entry = nil
371
374
 
372
- str << semi_comment.join("\n").gsub(/^#:\s*/, "#: ")
373
- if semi_comment.size > 0
374
- str << "\n"
375
- end
375
+ obsolete_entries = extract_obsolete_entries(result, definition)
376
+ unless obsolete_entries.empty?
377
+ obsolete_comment = ""
376
378
 
377
- if is_fuzzy
378
- str << "#, fuzzy\n"
379
+ obsolete_entries.each do |entry|
380
+ obsolete_comment << entry.to_s
381
+ end
382
+ obsolete_entry = POEntry.new(:normal)
383
+ obsolete_entry.msgid = :last
384
+ obsolete_entry.comment = obsolete_comment
379
385
  end
380
-
381
- str
386
+ obsolete_entry
382
387
  end
383
388
 
384
- def merge_header(target, definition)
385
- merge_comment("", target, "", definition)
386
-
387
- msg = target.msgstr("")
388
- def_msg = definition.msgstr("")
389
- if POT_DATE_EXTRACT_RE =~ msg
390
- time = $1
391
- def_msg = def_msg.sub(POT_DATE_RE, "POT-Creation-Date: #{time}")
389
+ def extract_obsolete_entries(result, definition)
390
+ obsolete_entries = []
391
+ definition.each do |entry|
392
+ id = [entry.msgctxt, entry.msgid]
393
+ unless result.has_key?(*id)
394
+ obsolete_entries << entry
395
+ end
392
396
  end
393
-
394
- target[""] = def_msg
397
+ obsolete_entries
395
398
  end
396
399
  end
397
400
 
@@ -500,10 +503,10 @@ module GetText
500
503
  def run(*options) #:nodoc:
501
504
  config = check_command_line_options(*options)
502
505
 
503
- parser = PoParser.new
506
+ parser = POParser.new
504
507
  parser.ignore_fuzzy = false
505
- defpo = parser.parse_file(config.defpo, PoData.new)
506
- refpot = parser.parse_file(config.refpot, PoData.new)
508
+ defpo = parser.parse_file(config.defpo, PO.new)
509
+ refpot = parser.parse_file(config.refpot, PO.new)
507
510
 
508
511
  merger = Merger.new
509
512
  result = merger.merge(defpo, refpot)
@@ -512,10 +515,10 @@ module GetText
512
515
 
513
516
  if config.output.is_a?(String)
514
517
  File.open(File.expand_path(config.output), "w+") do |file|
515
- file.write(result.generate_po)
518
+ file.write(result.to_s)
516
519
  end
517
520
  else
518
- puts(result.generate_po)
521
+ puts(result.to_s)
519
522
  end
520
523
  end
521
524
  end