neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

check.vim (9507B)


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