mirror of
https://github.com/rkd77/elinks.git
synced 2024-08-20 20:54:47 -04:00
The --msgid-fallback mode still reports conflicts between msgid and msgstr strings, but the resulting suggestions can now include accelerators reserved by msgids; thus the translator won't be lured into avoiding accelerators that she'll be likely to replace anyway. Therefore, --msgid-fallback is now the default and can be turned off with the new option --no-msgid-fallback. After checking each file, check-accelerator-conflicts.pl displays a one-line summary of how many accelerators it checked and how many conflicts it found. This is primarily meant to warn people by displaying zeroes if if gather-accelerator-contexts.pl has not been run. Locale::PO::dump doesn't output an msgid if there isn't one.
339 lines
12 KiB
Prolog
Executable File
339 lines
12 KiB
Prolog
Executable File
#! /usr/bin/perl
|
|
# The copyright notice and license are in the POD at the bottom.
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Locale::PO qw();
|
|
use Getopt::Long qw(GetOptions :config bundling gnu_compat);
|
|
use autouse 'Pod::Usage' => qw(pod2usage);
|
|
|
|
my $VERSION = "1.5";
|
|
|
|
sub show_version
|
|
{
|
|
print "check-accelerator-conflicts.pl $VERSION\n";
|
|
pod2usage({-verbose => 99, -sections => "COPYRIGHT AND LICENSE",
|
|
-exitval => 0});
|
|
}
|
|
|
|
# The character that precedes accelerators in strings.
|
|
# Set with the --accelerator-tag=CHARACTER option.
|
|
my $Opt_accelerator_tag;
|
|
|
|
# True if, for missing or fuzzy translations, the msgid string should
|
|
# be checked instead of msgstr. Set with the --msgid-fallback and
|
|
# --no-msgid-fallback options.
|
|
my $Opt_msgid_fallback = 1;
|
|
|
|
sub acceleration_arrays_eq ($$)
|
|
{
|
|
my($left, $right) = @_;
|
|
ref($left) eq "ARRAY" or return 0;
|
|
ref($right) eq "ARRAY" or return 0;
|
|
@$left == @$right or return 0;
|
|
$left->[$_] eq $right->[$_] or return 0
|
|
foreach (0 .. $#$right);
|
|
return 1;
|
|
}
|
|
|
|
sub check_po_file ($)
|
|
{
|
|
# The name of the PO file to be checked.
|
|
my($po_file_name) = @_;
|
|
|
|
# A nested hash that lists the accelerators and their uses.
|
|
# $accelerators{$accelerator}{$ctxname}{ACCELERATIONS}[$i]{LINENO}
|
|
# 1. In %accelerators, the keys are one-character strings,
|
|
# and the values are hash references.
|
|
# 2. In %{$accelerators{$accelerator}}, the keys are names
|
|
# of contexts in which the accelerator is used, and the
|
|
# values are references to "crossing" hashes.
|
|
# 3. %{$accelerators{$accelerator}{$ctxname}} is a "crossing" hash,
|
|
# so named because it describes how an accelerator and a context
|
|
# cross each other. It has the following elements:
|
|
# (ACCELERATIONS => [see point 4 below],
|
|
# REPORTED => (1 if this is a conflict and has been reported),
|
|
# AVOID => (1 if this accelerator should not be suggested in this
|
|
# context))
|
|
# 4. @{$accelerators{$accelerator}{$ctxname}{ACCELERATIONS}} is a list of
|
|
# references to "acceleration" hashes. If the same acceleration occurs
|
|
# in multiple contexts, then the references are to the same hash.
|
|
# 5. %{$accelerators{$accelerator}{ctxname}{ACCELERATIONS}[$i]} is an
|
|
# "acceleration" hash. It has the following structure:
|
|
# (PO => (the Locale::PO object),
|
|
# CTXNAMES => [read-only list of names of contexts in which the
|
|
# accelerator is used],
|
|
# ACCELERATOR => (a one-character string),
|
|
# LINENO => (line number in the PO file),
|
|
# STRING => (the msgid or msgstr string that defines the accelerator;
|
|
# unquoted as much as possible),
|
|
# EXPLAIN => (a string to be displayed if a conflict is found))
|
|
my %accelerators;
|
|
|
|
# How many entries had checkable accelerators.
|
|
my $checkable_count = 0;
|
|
|
|
# How many conflicts have been found so far.
|
|
my $conflict_count = 0;
|
|
|
|
{
|
|
my $pos = Locale::PO->load_file_asarray($po_file_name)
|
|
or warn "$po_file_name: $!\n", return 2;
|
|
foreach my $po (@$pos) {
|
|
my $automatic = $po->automatic()
|
|
or next;
|
|
my($ctxnames) = ($automatic =~ /^accelerator_context\(([^\)]*)\)/)
|
|
or next;
|
|
my @ctxnames = split(/\s*,\s*/, $ctxnames);
|
|
my @accelerations;
|
|
my $msgid = $po->msgid();
|
|
my $msgstr = $po->msgstr();
|
|
if ($po->dequote($msgstr) ne "" && !$po->fuzzy()) {
|
|
if (my($accelerator) = ($msgstr =~ /\Q$Opt_accelerator_tag\E(.)/s)) {
|
|
push @accelerations, { PO => $po,
|
|
CTXNAMES => \@ctxnames,
|
|
ACCELERATOR => $accelerator,
|
|
LINENO => $po->msgstr_begin_lineno(),
|
|
STRING => $msgstr,
|
|
EXPLAIN => "msgstr $msgstr" };
|
|
}
|
|
|
|
# TODO: look for accelerators in plural forms?
|
|
}
|
|
elsif ($Opt_msgid_fallback && $po->dequote($msgid) ne "") {
|
|
if (my($accelerator) = ($msgid =~ /\Q$Opt_accelerator_tag\E(.)/s)) {
|
|
push @accelerations, { PO => $po,
|
|
CTXNAMES => \@ctxnames,
|
|
ACCELERATOR => $accelerator,
|
|
LINENO => $po->msgid_begin_lineno(),
|
|
STRING => $msgid,
|
|
EXPLAIN => "msgid $msgid" };
|
|
}
|
|
}
|
|
|
|
$checkable_count++ if @accelerations;
|
|
foreach my $acceleration (@accelerations) {
|
|
my $accelerator = uc($acceleration->{ACCELERATOR});
|
|
foreach my $crossing (@{$accelerators{$accelerator}}{@ctxnames}) {
|
|
push @{$crossing->{ACCELERATIONS}}, $acceleration;
|
|
$crossing->{AVOID} = 1 if $acceleration->{EXPLAIN} =~ /^msgstr/;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach my $accelerator (sort keys %accelerators) {
|
|
my $ctxhash = $accelerators{$accelerator};
|
|
foreach my $crossing (@{$ctxhash}{sort keys %$ctxhash}) {
|
|
my $accelerations = $crossing->{ACCELERATIONS};
|
|
if ($accelerations && @$accelerations > 1 && !$crossing->{REPORTED}) {
|
|
my @ctxnames_in_conflict;
|
|
foreach my $inner_ctxname (keys %$ctxhash) {
|
|
my $inner_crossing = $ctxhash->{$inner_ctxname};
|
|
if (acceleration_arrays_eq($inner_crossing->{ACCELERATIONS},
|
|
$accelerations)) {
|
|
push @ctxnames_in_conflict, $inner_ctxname;
|
|
$inner_crossing->{REPORTED} = 1;
|
|
}
|
|
}
|
|
my $ctxnames_in_conflict = join(", ", map(qq("$_"), @ctxnames_in_conflict));
|
|
warn "$po_file_name: Accelerator conflict for \"$accelerator\" in $ctxnames_in_conflict:\n";
|
|
foreach my $acceleration (@$accelerations) {
|
|
my $lineno = $acceleration->{LINENO};
|
|
my $explain = $acceleration->{EXPLAIN};
|
|
|
|
# Get a string of unique characters in the string,
|
|
# preferring characters that start a word.
|
|
# FIXME: should remove quotes and resolve \n etc.
|
|
my $displaystr = $acceleration->{STRING};
|
|
$displaystr =~ s/\Q$Opt_accelerator_tag\E//g;
|
|
my $suggestions = "";
|
|
foreach my $char ($displaystr =~ /\b(\w)/g,
|
|
$displaystr =~ /(\w)/g) {
|
|
$suggestions .= $char unless $suggestions =~ /\Q$char\E/i;
|
|
}
|
|
|
|
# But don't suggest unavailable characters.
|
|
SUGGESTION: foreach my $char (split(//, $suggestions)) {
|
|
foreach my $ctxname (@{$acceleration->{CTXNAMES}}) {
|
|
$suggestions =~ s/\Q$char\E//, next SUGGESTION
|
|
if $accelerators{uc($char)}{$ctxname}{AVOID};
|
|
}
|
|
}
|
|
|
|
warn "$po_file_name:$lineno: $explain\n";
|
|
if ($suggestions eq "") {
|
|
warn "$po_file_name:$lineno: no suggestions :-(\n";
|
|
}
|
|
else {
|
|
warn "$po_file_name:$lineno: suggestions: $suggestions\n";
|
|
}
|
|
} # foreach $acceleration in conflict
|
|
$conflict_count++;
|
|
} # if found a conflict
|
|
} # foreach context known for $accelerator
|
|
} # foreach $accelerator
|
|
|
|
print "$po_file_name: "
|
|
. ($checkable_count == 1 ? "Checked 1 entry" : "Checked $checkable_count entries")
|
|
. ", "
|
|
. ($conflict_count == 1 ? "found 1 accelerator conflict" : "found $conflict_count accelerator conflicts")
|
|
. ".\n";
|
|
return $conflict_count ? 1 : 0;
|
|
}
|
|
|
|
GetOptions("accelerator-tag=s" => sub {
|
|
my($option, $value) = @_;
|
|
die "Cannot use multiple --accelerator-tag options\n"
|
|
if defined($Opt_accelerator_tag);
|
|
die "--accelerator-tag requires a single-character argument\n"
|
|
if length($value) != 1;
|
|
$Opt_accelerator_tag = $value;
|
|
},
|
|
"msgid-fallback!" => \$Opt_msgid_fallback,
|
|
"help" => sub { pod2usage({-verbose => 1, -exitval => 0}) },
|
|
"version" => \&show_version)
|
|
or exit 2;
|
|
$Opt_accelerator_tag = "~" unless defined $Opt_accelerator_tag;
|
|
print(STDERR "$0: missing file operand\n"), exit 2 unless @ARGV;
|
|
|
|
my $max_error = 0;
|
|
foreach my $po_file_name (@ARGV) {
|
|
my $error = check_po_file($po_file_name);
|
|
$max_error = $error if $error > $max_error;
|
|
}
|
|
exit $max_error;
|
|
|
|
__END__
|
|
|
|
=head1 NAME
|
|
|
|
check-accelerator-conflicts.pl - Scan a PO file for conflicting
|
|
accelerator keys.
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
B<check-accelerator-conflicts.pl> [I<option> ...] F<I<language>.po> [...]
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
B<check-accelerator-conflicts.pl> is part of a framework that detects
|
|
conflicting accelerator keys in Gettext PO files. A conflict is when
|
|
two items in the same menu or two buttons in the same dialog box use
|
|
the same accelerator key.
|
|
|
|
The PO file format does not normally include any information on which
|
|
strings will be used in the same menu or dialog box.
|
|
B<check-accelerator-conflicts.pl> can only be used on PO files to which
|
|
this information has been added with B<gather-accelerator-contexts.pl>
|
|
or merged with B<msgmerge>.
|
|
|
|
B<check-accelerator-conflicts.pl> reads the F<I<language>.po> file
|
|
named on the command line and reports any conflicts to standard error.
|
|
It also tries to suggest replacements for the conflicting accelerators.
|
|
|
|
B<check-accelerator-conflicts.pl> does not access the source files to
|
|
which F<I<language>.po> refers. Thus, it does not matter if the line
|
|
numbers in "#:" lines are out of date.
|
|
|
|
=head1 OPTIONS
|
|
|
|
=over
|
|
|
|
=item B<--accelerator-tag=>I<character>
|
|
|
|
Specify the character that marks accelerators in C<msgstr> strings.
|
|
Whenever this character occurs in a C<msgstr>,
|
|
B<check-accelerator-conflicts.pl> treats the next character as an
|
|
accelerator and checks that it is unique in each of the contexts in
|
|
which the C<msgstr> is used.
|
|
|
|
Omitting the B<--accelerator-tag> option implies
|
|
B<--accelerator-tag="~">. The option must be given to each program
|
|
separately because there is no standard way to save this information
|
|
in the PO file.
|
|
|
|
=item B<--msgid-fallback>
|
|
|
|
=item B<--no-msgid-fallback>
|
|
|
|
Select how to check entries where the C<msgstr> is missing or fuzzy.
|
|
The default is B<--msgid-fallback>, which makes
|
|
B<check-accelerator-conflicts.pl> use the C<msgid> instead, and report
|
|
any conflicts between C<msgid> and C<msgstr> strings. The alternative
|
|
is B<--no-msgid-fallback>, which makes B<check-accelerator-conflicts.pl>
|
|
completely ignore such entries.
|
|
|
|
Regardless of these options, B<check-accelerator-conflicts.pl> will
|
|
suggest accelerators that would conflict with ones defined in C<msgid>
|
|
strings. Those strings will be eventually shadowed by C<msgstr>
|
|
strings, so their accelerators should not affect which accelerators
|
|
the translator chooses for C<msgstr> strings.
|
|
|
|
=back
|
|
|
|
=head1 ARGUMENTS
|
|
|
|
=over
|
|
|
|
=item F<I<language>.po> [...]
|
|
|
|
The PO files to be scanned for conflicts. These files must include the
|
|
"accelerator_context" comments added by B<gather-accelerator-contexts.pl>.
|
|
If the special comments are missing, no conflicts will be found.
|
|
|
|
=back
|
|
|
|
=head1 EXIT CODE
|
|
|
|
0 if no conflicts were found.
|
|
|
|
1 if some conflicts were found.
|
|
|
|
2 if the command line is invalid or a file cannot be read.
|
|
|
|
=head1 BUGS
|
|
|
|
=head2 Waiting for Locale::PO fixes
|
|
|
|
When B<check-accelerator-conflicts.pl> includes C<msgstr> strings in
|
|
warnings, it should transcode them from the charset of the PO file to
|
|
the one specified by the user's locale.
|
|
|
|
=head1 AUTHOR
|
|
|
|
Kalle Olavi Niemitalo <kon@iki.fi>
|
|
|
|
=head1 COPYRIGHT AND LICENSE
|
|
|
|
Copyright (c) 2005-2006 Kalle Olavi Niemitalo.
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the same terms as Perl itself. In addition:
|
|
|
|
=over
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a
|
|
copy of this software and associated documentation files (the "Software"),
|
|
to deal in the Software without restriction, including without limitation
|
|
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
and/or sell copies of the Software, and to permit persons to whom the
|
|
Software is furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
DEALINGS IN THE SOFTWARE.
|
|
|
|
=back
|
|
|
|
=head1 SEE ALSO
|
|
|
|
L<gather-accelerator-contexts.pl>, C<xgettext(1)>, C<msgmerge(1)>
|