#!/usr/bin/env perl -w # Po4a::VimHelp.pm # # extract and translate translatable strings from a Vim help files # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # ######################################################################## package Locale::Po4a::VimHelp; use 5.006; use strict; use warnings; use parent qw(Locale::Po4a::TransTractor); sub initialize { my ( $self, %options ) = @_; $self->{debug} = $options{debug}; return; } sub parse { my $self = shift; $self->translate_firstline(); my ( $line, $ref ) = $self->shiftline(); while ( defined($line) ) { chomp $line; $self->skip_separator($line) or $self->translate_columnheading( $line, $ref ) or $self->skip_tags($line) or $self->skip_modeline($line) or $self->skip_blank($line) or $self->translate_codeblock_with_start_line($line) or $self->translate_paragraph( $line, $ref ); ( $line, $ref ) = $self->shiftline(); } } sub translate_firstline { my $self = shift; my ( $line, $ref ) = $self->shiftline(); chomp $line; $line =~ / \A ([*] [^*]+ [*] \s+) (.*) /xms or warn "no first line"; my $pre = $1; my $description = $2; $description = $self->translate( $description, $ref, "description" ); $self->pushline("$pre$description\n"); return; } sub skip_separator { my ( $self, $line ) = @_; is_separator($line) or return; $self->pushline("$line\n"); return 1; } sub is_separator { return shift =~ / \A \s* ([=]+ | [-]+) \Z /xms; } sub translate_columnheading { my ( $self, $line, $ref ) = @_; my ( $content, $suffix ) = is_columnheading($line) or return; my $following_ref; ( $line, $following_ref ) = $self->shiftline(); while ( defined $line ) { chomp $line; my ($following_content) = is_columnheading($line); if ( !$following_content ) { $self->unshiftline( "$line\n", $following_ref ); last; } $content .= "\n$following_content"; ( $line, $following_ref ) = $self->shiftline(); } $content = $self->translate( $content, $ref, "column heading" ); $content =~ s/ $ /$suffix/xmsg; $self->pushline("$content\n"); return 1; } sub is_columnheading { my $line = shift; $line =~ / \A (.+?) (\s* [~]) \Z /xms or return; my $content = $1; my $suffix = $2; return ( $content, $suffix ); } sub skip_tags { my ( $self, $line ) = @_; my $result = is_tags($line) or return; $self->pushline("$line\n"); $result->{codeblock} and $self->translate_codeblock(); return 1; } sub is_tags { my $line = shift; $line =~ / \A \s* [*] [^*]+ [*] (?: \s+ [*] [^*]+ [*] )* (\s+ [>] (vim)?)? \Z /xms or return; my $codeblock = $1; return { codeblock => $codeblock }; } sub skip_modeline { my ( $self, $line ) = @_; $line =~ / \A \s* vim: /xms or return; $self->pushline("$line\n"); return 1; } sub translate_codeblock_with_start_line { my ( $self, $line ) = @_; is_codeblock_start_line($line) or return; $self->pushline("$line\n"); $self->translate_codeblock(); return 1; } sub is_codeblock_start_line { return shift =~ / \A [>] (vim)? \Z /xms; } sub translate_paragraph { my ( $self, $line, $initial_ref, $type ) = @_; $type //= "paragraph"; my ( $content, $codeblock ) = parse_paragraph_line($line); if ($codeblock) { $content = $self->translate( $content, $initial_ref, $type ); $self->pushline("$content$codeblock\n"); $self->translate_codeblock(); return 1; } my @content = $content; my $ref; ( $line, $ref ) = $self->shiftline(); while ( defined $line ) { chomp $line; if ( paragraph_breakable($line) ) { $self->unshiftline( $line, $ref ); last; } my $following_content; ( $following_content, $codeblock ) = parse_paragraph_line($line); push @content, $following_content; $codeblock and last; ( $line, $ref ) = $self->shiftline(); } $content = $self->translate_indented( \@content, $initial_ref, $type ); $self->pushline("$content$codeblock\n"); $codeblock and $self->translate_codeblock(); return 1; } sub parse_paragraph_line { my $line = shift; $line =~ / \A (.*?) (\s+ [>] (vim)?)? \Z /xms or die "unreachable"; my $content = $1; my $codeblock = $2; return ( $content, $codeblock // "" ); } sub paragraph_breakable { my $line = shift; return is_blank($line) || is_separator($line) || is_codeblock_start_line($line); } sub translate_codeblock { my ($self) = @_; my ( $line, $ref ) = $self->shiftline(); while ( defined $line ) { chomp $line; is_blank($line) or last; $self->pushline("$line\n"); ( $line, $ref ) = $self->shiftline(); } my $initial_ref = $ref; my @content; while ( defined $line ) { chomp $line; $initial_ref //= $ref; if ( $line =~ / \A ([<]) (.*) /xms ) { my $end = $1; $line = $2; if (@content) { my $content = $self->translate_indented( \@content, $initial_ref, "codeblock" ); $self->pushline("$content\n$end"); } else { $self->pushline($end); } $self->unshiftline( $line, $ref ); last; } elsif ( $line =~ / \A \S /xms ) { if (@content) { my $content = $self->translate_indented( \@content, $initial_ref, "codeblock" ); $self->pushline("$content\n"); } $self->unshiftline( $line, $ref ); last; } elsif ( is_blank($line) ) { if (@content) { my $content = $self->translate_indented( \@content, $initial_ref, "codeblock" ); $self->pushline("$content\n"); undef @content; undef $initial_ref; } $self->pushline("$line\n"); } else { push @content, $line; } ( $line, $ref ) = $self->shiftline(); } return; } sub translate_indented { my ( $self, $content, $ref, $type ) = @_; my $indent = 0; my $firstline = shift @{$content}; for my $char_index ( 0 .. length($firstline) - 1 ) { my $char = substr( $firstline, $char_index, 1 ) or last; $char =~ / \s /xms or last; my $common = 1; for my $line ( @{$content} ) { substr( $line, $char_index, 1 ) eq $char and next; undef $common; last; } $common or last; $indent += 1; } my $prefix = substr( $firstline, 0, $indent ); unshift @{$content}, $firstline; @{$content} = map { substr( $_, $indent ) } @{$content}; my $translation = $self->translate( join( "\n", @{$content} ), $ref, $type ); $translation =~ s/ ^ /$prefix/xmsg; return $translation; } sub skip_blank { my ( $self, $line ) = @_; is_blank($line) or return; $self->pushline("$line\n"); return 1; } sub is_blank { my $line = shift; return !$line || $line =~ / \A \s* \Z /xms; } 1; __END__ =encoding UTF-8 =head1 NAME Locale::Po4a::VimHelp - convert Vim help files from/to PO files. =head1 DESCRIPTION The po4a (PO for anything) project goal is to ease translations (and more interestingly, the maintenance of translations) using gettext tools on areas where they were not expected like documentation. C is a module to help the translation of Vim help file. See also L for its syntax. =head1 STATUS OF THIS MODULE This module is in an early stage of development. It has been successfully tested on simple files like C. However, it has not yet been tested on full help files, and the way it parses them may change for fixes and improvements, especially paragraph wrapping. =head1 DEVELOPING Parsing Vim help files for po4a is difficult. Here are some obstacles while developing this module. If you have a better idea, feel free to suggest a patch (with additional test cases). =over =item C<1. foo> sounds like an ordered list Not necessarily. Consider the following paragraph. There are 2 wandering tanukis, not 1. He's using his cloning technique. For the same reason, what appears to be a heading (e.g. C<1.1 bar>) or an unordered list (e.g. C) cannot be detected. =item Flushed right tags should be excluded from translation target This topic is also difficult since it relates to language differences in localization. Consider the following example. Such a common and short concept in some languages *tags* which might be translated to They are unfamiliar, there are no corresponding concepts and *tags* it takes a longer sentence to express them in this language When this happens, you have to decide at what point you want to add a tag I. This shouldn't be easy. The Vim help file is in a hard wrapped format, even for parts that are not code blocks. The same applies to tag references such as C<|ref|>. =item Leveraging C<--wrapcol> to monitor text widths Using the C<--wrapcol> option to issue warnings when text width exceeds specified limits seems like a reasonable approach. The official Vim help files feature a mode line, such as C, as seen in L|https://github.com/vim/vim/blob/04cc8975930b7b2c5d6753d3eddf57dab2816518/runtime/doc/if_perl.txt#L307>. However, there are a few challenges to consider: =over =item File Parsing Behavior If this option is implemented, it would be ideal to read the entire file first, detect whether the mode line exists, and determine which sections are affected. Unfortunately, the progressive parsing mechanism is not employed in this case. =item Default vs. Custom Values The generic default value of C<--wrapcol> is 76, whereas Vim help files commonly use 78. In most cases, the value in the options passed to this module defaults to 76, but it is unclear whether this value was explicitly set or implicitly applied. =back =back =head1 SEE ALSO L, L =head1 AUTHORS gemmaro =head1 COPYRIGHT AND LICENSE Copyright © 2024, 2025 gemmaro. This program is free software; you may redistribute it and/or modify it under the terms of GPL v2.0 or later (see the F file).