1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
|
" MNV script for checking .po files.
"
" Goes through the xx.po file (more than once)
" and verify various congruences
" See the comments in the code
" Last Update: 2025 Aug 06
if 1 " Only execute this if the eval feature is available.
" Using line continuation (set cpo to mnv default value)
let s:save_cpo = &cpo
set cpo&mnv
" This only works when 'wrapscan' is not set.
let s:save_wrapscan = &wrapscan
set nowrapscan
" Function to get a split line at the cursor.
" Used for both msgid and msgstr lines.
" Removes all text except % items and returns the result.
func! GetMline()
let idline = substitute(getline('.'), '"\(.*\)"$', '\1', '')
while line('.') < line('$')
silent +
let line = getline('.')
if line[0] != '"'
break
endif
let idline .= substitute(line, '"\(.*\)"$', '\1', '')
endwhile
" remove '%', not used for formatting.
let idline = substitute(idline, "'%'", '', 'g')
let idline = substitute(idline, "%%", '', 'g')
" remove '%' used for plural forms.
let idline = substitute(idline, '\\nPlural-Forms: .\+;\\n', '', '')
" remove duplicate positional format arguments
let idline2 = ""
while idline2 != idline
let idline2 = idline
let idline = substitute(idline, '%\([1-9][0-9]*\)\$\([-+ #''.*]*[0-9]*l\=[dsuxXpoc%]\)\(.*\)%\1$\([-+ #''.*]*\)\(l\=[dsuxXpoc%]\)', '%\1$\2\3\4', 'g')
endwhile
" remove everything but % items.
return substitute(idline, '[^%]*\(%([1-9][0-9]*\$)\=[-+ #''.0-9*]*l\=[dsuxXpoc%]\)\=', '\1', 'g')
endfunc
func! CountNl(first, last)
let nl = 0
for lnum in range(a:first, a:last)
let nl += count(getline(lnum), "\n")
endfor
return nl
endfunc
" main
" Start at the first "msgid" line.
let wsv = winsaveview()
silent 1
silent keeppatterns /^msgid\>
" When an error is detected this is set to the line number.
" Note: this is used in the Makefile.
let error = 0
while 1
" for each "msgid"
" check msgid "Text;editor;"
" translation must have two or more ";" (in case of more categories)
let lnum = line('.')
if getline(lnum) =~ 'msgid "Text;.*;"'
if getline(lnum + 1) !~ '^msgstr "\([^;]\+;\)\+"$'
echomsg 'Mismatching ; in line ' . (lnum + 1)
echomsg 'Wrong semicolon count'
if error == 0
let error = lnum + 1
endif
endif
endif
" check for equal number of % in msgid and msgstr
" it is skipping the no-c-format strings
if getline(line('.') - 1) !~ "no-c-format"
" skip the "msgid_plural" lines
let prevfromline = 'foobar'
let plural = 0
while 1
if getline('.') =~ 'msgid_plural'
let plural += 1
endif
let fromline = GetMline()
if prevfromline != 'foobar' && prevfromline != fromline
\ && (plural != 1
\ || count(prevfromline, '%') + 1 != count(fromline, '%'))
echomsg 'possibly mismatching % in line ' . (line('.') - 1)
echomsg 'msgid: ' . prevfromline
echomsg 'msgid: ' . fromline
if error == 0
let error = line('.')
endif
endif
if getline('.') !~ 'msgid_plural'
break
endif
let prevfromline = fromline
endwhile
" checks that for each 'msgid' there is a 'msgstr'
if getline('.') !~ '^msgstr'
echomsg 'Missing "msgstr" in line ' . line('.')
if error == 0
let error = line('.')
endif
endif
" check all the 'msgstr' lines have the same number of '%'
" only the number of '%' is checked,
" %d vs. %s or %d vs. %ld go undetected
while getline('.') =~ '^msgstr'
let toline = GetMline()
if fromline != toline
\ && (plural == 0 || count(fromline, '%') != count(toline, '%') + 1)
echomsg 'possibly mismatching % in line ' . (line('.') - 1)
echomsg 'msgid: ' . fromline
echomsg 'msgstr: ' . toline
if error == 0
let error = line('.')
endif
endif
if line('.') == line('$')
break
endif
endwhile
endif
" Find next msgid. Quit when there is no more.
let lnum = line('.')
silent! keeppatterns /^msgid\>
if line('.') == lnum
break
endif
endwhile
" Check that error code in msgid matches the one in msgstr.
"
" Examples of mismatches found with msgid "E123: ..."
" - msgstr "E321: ..." error code mismatch
" - msgstr "W123: ..." warning instead of error
" - msgstr "E123 ..." missing colon
" - msgstr "..." missing error code
"
silent 1
if search('msgid "\("\n"\)\?\([EW][0-9]\+:\).*\nmsgstr "\("\n"\)\?[^"]\@=\2\@!') > 0
echomsg 'Mismatching error/warning code in line ' . line('.')
if error == 0
let error = line('.')
endif
endif
" Check that the \n at the end of the msgid line is also present in the msgstr
" line. Skip over the header.
silent 1
silent keeppatterns /^"MIME-Version:
while 1
let lnum = search('^msgid\>')
if lnum <= 0
break
endif
let strlnum = search('^msgstr\>')
let end = search('^$')
if end <= 0
let end = line('$') + 1
endif
let origcount = CountNl(lnum, strlnum - 1)
let transcount = CountNl(strlnum, end - 1)
" Allow for a few more or less line breaks when there are 2 or more
if origcount != transcount && (origcount <= 2 || transcount <= 2)
echomsg 'Mismatching "\n" in line ' . line('.')
if error == 0
let error = lnum
endif
endif
endwhile
" Check that the eventual continuation of 'msgstr' is well formed
" final '""', '\n"', ' "' '/"' '."' '-"' are OK
" Beware, it can give false positives if the message is split
" in the middle of a word
silent 1
silent keeppatterns /^"MIME-Version:
while 1
let lnum = search('^msgid\>')
if lnum <= 0
break
endif
" "msgstr" goes from strlnum to end-1
let strlnum = search('^msgstr\>')
let end = search('^$')
if end <= 0
let end = line('$') + 1
endif
" only if there is a continuation line...
if end > strlnum + 1
let ilnum = strlnum
while ilnum < end - 1
let iltype = 0
if getline( ilnum ) =~ "^msgid_plural"
let iltype = 2
endif
if getline( ilnum ) =~ "^msgstr["
let iltype = 2
endif
if getline( ilnum ) =~ "\"\""
let iltype = 1
endif
if getline( ilnum ) =~ " \"$"
let iltype = 1
endif
if getline( ilnum ) =~ "-\"$"
let iltype = 1
endif
if getline( ilnum ) =~ "/\"$"
let iltype = 1
endif
if getline( ilnum ) =~ "\\.\"$"
let iltype = 1
endif
if getline( ilnum ) =~ "\\\\n\"$"
let iltype = 1
endif
if iltype == 0
echomsg 'Possibly incorrect final at line: ' . ilnum
" TODO: make this an error
" if error == 0
" let error = ilnum
" endif
endif
let ilnum += 1
endwhile
endif
endwhile
" Check that the file is well formed according to msgfmts understanding
if executable("msgfmt")
let filename = expand("%")
" Newer msgfmt does not take OLD_PO_FILE_INPUT argument, must be in
" environment.
let $OLD_PO_FILE_INPUT = 'yes'
let a = system("msgfmt --statistics " . filename)
if v:shell_error != 0
let error = matchstr(a, filename.':\zs\d\+\ze:')+0
for line in split(a, '\n') | echomsg line | endfor
endif
endif
" Check that the plural form is properly initialized
silent 1
let plural = search('^msgid_plural ', 'n')
if (plural && search('^"Plural-Forms: ', 'n') == 0) || (plural && search('^msgstr\[0\] ".\+"', 'n') != plural + 1)
if search('^"Plural-Forms: ', 'n') == 0
echomsg "Missing Plural header"
if error == 0
let error = search('\(^"[A-Za-z-_]\+: .*\\n"\n\)\+\zs', 'n') - 1
endif
elseif error == 0
let error = plural
endif
elseif !plural && search('^"Plural-Forms: ', 'n')
" We allow for a stray plural header, msginit adds one.
endif
" Check that 8bit encoding is used instead of 8-bit
let cte = search('^"Content-Transfer-Encoding:\s\+8-bit', 'n')
let ctc = search('^"Content-Type:.*;\s\+\<charset=[iI][sS][oO]_', 'n')
let ctu = search('^"Content-Type:.*;\s\+\<charset=utf-8', 'n')
if cte
echomsg "Warn: Content-Transfer-Encoding should be 8bit instead of 8-bit"
" TODO: make this an error
" if error == 0
" let error = cte
" endif
elseif ctc
echomsg "Warn: Content-Type charset should be 'ISO-...' instead of 'ISO_...'"
" TODO: make this an error
" if error == 0
" let error = ct
" endif
elseif ctu
echomsg "Warn: Content-Type charset should be 'UTF-8' instead of 'utf-8'"
" TODO: make this an error
" if error == 0
" let error = ct
" endif
endif
" Check that no lines are longer than 80 chars (except comments)
let overlong = search('^[^#]\%>80v', 'n')
if overlong > 0
echomsg "Warn: Lines should be wrapped at 80 columns"
" TODO: make this an error
" if error == 0
" let error = overlong
" endif
endif
" Check that there is no trailing whitespace
let overlong = search('\s\+$', 'n')
if overlong > 0
echomsg $"Warn: Trailing whitespace at line: {overlong}"
" TODO: make this an error
" if error == 0
" let error = overlong
" endif
endif
if error == 0
" If all was OK restore the view.
call winrestview(wsv)
echomsg "OK"
else
" Put the cursor on the line with the error.
exe error
endif
" restore original wrapscan
let &wrapscan = s:save_wrapscan
unlet s:save_wrapscan
" restore original cpo
let &cpo = s:save_cpo
unlet s:save_cpo
endif
" mnv:sts=2:sw=2:et
|