The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#! /bin/false
# vim: ts=4:et

# Copyright (C) 2016-2017 Guido Flohr <guido.flohr@cantanea.com>,
# all rights reserved.

# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU Library General Public License as published
# by the Free Software Foundation; either version 2, 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 warrant y of
# MERCHANTABILITY or  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Library General Public License for more details.

# You should have received a copy of the GNU Library General Public
# License along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
# USA.

# ABSTRACT: Extract Strings To PO Files

package Locale::XGettext;
$Locale::XGettext::VERSION = '0.3';
use strict;

use Locale::TextDomain 1.20 qw(Locale-XGettext);
use File::Spec;
use Locale::PO 0.27;
use Scalar::Util qw(reftype blessed);
use Locale::Recode;
use Getopt::Long 2.36 qw(GetOptionsFromArray);

use Locale::XGettext::Util::POEntries;
use Locale::XGettext::Util::Keyword;
use Locale::XGettext::Util::Flag;

# Helper method, not exported!
sub __empty($) {
    my ($what) = @_;

    return if defined $what && length $what;

    return 1;
}

sub new {
    my ($class, $options, @files) = @_;

    my $self;
    if (ref $class) {
        $self = $class;
    } else {
        $self = bless {}, $class;
    }

    $self->{__options} = $options;
    $self->{__comment_tag} = undef;
    $self->{__files} = [@files];
    $self->{__exclude} = {};

    if (__PACKAGE__ eq ref $self) {
        require Carp;
        Carp::croak(__x("{package} is an abstract base class and must not"
                        . " be instantiated directly",
                        package => __PACKAGE__));
    }
    
    $options->{default_domain} = 'messages' if __empty $options->{default_domain};
    $options->{from_code} = 'ASCII' if __empty $options->{default_domain};
    $options->{output_dir} = '.' if __empty $options->{output_dir};

    if (exists $options->{add_location}) {
        my $option = $options->{add_location};
        if (__empty $option) {
            $option = 'full';
        }
        die __"The argument to '--add-location' must be 'full', 'file', or 'never'.\n"
            if $option ne 'full' && $option ne 'file' && $option ne 'never';
    }

    if (exists $options->{add_comments}) {
        if (!ref $options->{add_comments} 
            && 'ARRAY' ne $options->{add_comments}) {
            die __"Option 'add_comments' must be an array reference.\n";
        }
        
        foreach my $comment (@{$options->{add_comments}}) {
            $comment =~ s/^[ \t\n\f\r\013]+//;
            $comment = quotemeta $comment;
        }
    }
    
    $options->{from_code} = 'ASCII' if __empty $options->{from_code};

    my $from_code = $options->{from_code};
    my $cd = Locale::Recode->new(from => $from_code,
                                 to => 'utf-8');
    if ($cd->getError) {        
        warn __x("warning: '{from_code}' is not a valid encoding name.  "
                 . "Using ASCII as fallback.",
                 from_code => $from_code);
        $options->{from_code} = 'ASCII';
    } else {
        $options->{from_code} = 
            Locale::Recode->resolveAlias($options->{from_code});
    }
    
    $self->__readFilesFrom($options->{files_from});
    if ($self->needInputFiles) {
        $self->__usageError(__"no input file given") if !@{$self->{__files}};
    }
    
    $self->{__keywords} = $self->__setKeywords($options->{keyword});
    $self->{__flags} = $self->__setFlags($options->{flag});

    if (exists $options->{exclude_file} && !ref $options->{exclude_file}) {
        $options->{exclude_file} = [$options->{exclude_file}];
    }

    $self->__readExcludeFiles($options->{exclude_file});

    return $self;
}

sub newFromArgv {
    my ($class, $argv) = @_;

    my $self;
    if (ref $class) {
        $self = $class;
    } else {
        $self = bless {}, $class;
    }

    my %options = eval { $self->__getOptions($argv) };
    if ($@) {
        $self->__usageError($@);
    }
    
    $self->__displayUsage if $options{help};
    
    if ($options{version}) {
        print $self->versionInformation;
        exit 0;
    }
    
    return $class->new(\%options, @$argv);
}

sub defaultKeywords {
    return [];
}

sub defaultFlags {
    return [];
}

sub run {
    my ($self) = @_;

    if ($self->{__run}++) {
        require Carp;
        Carp::croak(__"Attempt to re-run extractor");
    }

    my $po = $self->{__po} = Locale::XGettext::Util::POEntries->new;
    
    if ($self->option('join_existing')) {
        my $output_file = $self->__outputFilename;
        if ('-' eq $output_file) {
            $self->__usageError(__"--join-existing cannot be used when output"
                                  . " is written to stdout");
        }
        $self->readPO($output_file);
    }
    
    foreach my $filename (@{$self->{__files}}) {
        my $path = $self->resolveFilename($filename)
            or die __x("Error resolving file name '{filename}': {error}!\n",
                       filename => $filename, error => $!);
        if ($path =~ /\.pot?$/i) {
            $self->readPO($path);
        } else {
            $self->readFile($path);
        }
    }

    $self->extractFromNonFiles;
    
    # FIXME! Sort po!
    
    if (($po->entries || $self->{__options}->{force_po})
        && !$self->{__options}->{omit_header}) {
        $po->prepend($self->__poHeader);
    }

    foreach my $entry ($po->entries) {
        $self->recodeEntry($entry);
    }

    return $self;
}

sub extractFromNonFiles { shift }

sub resolveFilename {
    my ($self, $filename) = @_;
    
    my $directories = $self->{__options}->{directory} || [''];
    foreach my $directory (@$directories) {
        my $path = length $directory 
            ? File::Spec->catfile($directory, $filename) : $filename;
        stat $path && return $path;
    }
    
    return;
}

sub po {
    shift->{__po}->entries;
}

sub readPO {
    my ($self, $path) = @_;
    
    my $entries = Locale::PO->load_file_asarray($path)
        or die __x("error reading '{filename}': {error}!\n",
                   filename => $path, error => $!);
    
    foreach my $entry (@$entries) {
        if ('""' eq $entry->msgid
            && __empty $entry->dequote($entry->msgctxt)) {
            next;
        }
        $self->addEntry($entry);
    }
    
    return $self;
}

sub addEntry {
    my ($self, $entry) = @_;

    if (!$self->{__run}) {
        require Carp;
        # TRANSLATORS: run() is a method that should be invoked first. 
        Carp::croak(__"Attempt to add entries before run");
    }

    # Simplify calling from languages that do not have hashes.
    if (!ref $entry) {
        $entry = {splice @_, 1};
    }

    my $comment = delete $entry->{automatic};
    $entry = $self->__promoteEntry($entry);

    if (defined $comment) {
        # Does it contain an "xgettext:" comment?  The original implementation
        # is quite relaxed here, even recogizing comments like "exgettext:".
        my $cleaned = '';
        $comment =~ s{
                  (.*?)xgettext:(.*?(?:\n|\Z))
              }{
                  my ($lead, $string) = ($1, $2);
                  my $valid;
                            
                  my @tokens = split /[ \x09-\x0d]+/, $string;
                            
                  foreach my $token (@tokens) {
                      if ($token eq 'fuzzy') {
                          $entry->fuzzy(1);
                          $valid = 1;
                      } elsif ($token eq 'no-wrap') {
                            $entry->add_flag('no-wrap');
                            $valid = 1;
                      } elsif ($token eq 'wrap') {
                          $entry->add_flag('wrap');
                          $valid = 1;
                      } elsif ($token =~ /^[a-z]+-(?:format|check)$/) {
                            $entry->add_flag($token);
                            $valid = 1;
                      }
                  }
                            
                  $cleaned .= "${lead}xgettext:${string}" if !$valid;
              }exg;

        $cleaned .= $comment;
        $comment = $cleaned;

        my $comment_keywords = $self->option('add_comments');
        if (!__empty $comment && defined $comment_keywords) {
            my @automatic;
            foreach my $keyword (@$comment_keywords) {
                if ($comment =~ /($keyword.*)/s) {
                    push @automatic, $1;
                    last;
                }
            }
            
            my $old_automatic = $entry->automatic;
            push @automatic, $entry->dequote($old_automatic) if !__empty $old_automatic;
            $entry->automatic(join "\n", @automatic) if @automatic;    
        }
    }
    
    my ($msgid) = $entry->msgid;
    if (!__empty $msgid) {
        my $ctx = $entry->msgctxt;
        $ctx = '' if __empty $ctx;
        
        return $self if exists $self->{__exclude}->{$msgid}->{$ctx};
    }
    
    $self->{__po}->add($entry);

    return $self;
}

sub keywords {
    my ($self) = @_;

    my %keywords = %{$self->{__keywords}};

    return \%keywords;
}

sub keywordOptionStrings {
    my ($self) = @_;

    my @keywords;
    my $keywords = $self->keywords;
    foreach my $function (keys %$keywords) {
        push @keywords, $keywords->{$function}->dump;
    }

    return \@keywords;
}

sub flags {
    my ($self) = @_;

    my @flags = @{$self->{__flags}};

    return \@flags;
}

sub flagOptionStrings {
    my ($self) = @_;

    my @flags;
    my $flags = $self->flags;
    foreach my $flag (@$flags) {
        push @flags, $flag->dump;
    }

    return \@flags;
}

sub recodeEntry {
    my ($self, $entry) = @_;
    
    my $from_code = $self->option('from_code');
    $from_code = 'US-ASCII' if __empty $from_code;
    $from_code = Locale::Recode->resolveAlias($from_code);
    
    my $cd;
    if ($from_code ne 'US-ASCII' && $from_code ne 'UTF-8') {
        $cd = Locale::Recode->new(from => $from_code, to => 'utf-8');
        die $cd->getError if defined $cd->getError;
    }

    my $toString = sub {
        my ($entry) = @_;

        return join '', map { defined $_ ? $_ : '' }
            $entry->msgid, $entry->msgid_plural, 
            $entry->msgctxt, $entry->comment;
    };
    
    if ($from_code eq 'US-ASCII') {        
        # Check that everything is 7 bit.
        my $flesh = $toString->($entry);
        if ($flesh !~ /^[\000-\177]*$/) {
            die __x("Non-ASCII string at '{reference}'.\n"
                    . "    Please specify the source encoding through "
                    . "'--from-code'.\n",
                    reference => $entry->reference);
        }
    } elsif ($from_code eq 'UTF-8') {
        # Check that utf-8 is valid.
        require utf8; # [SIC!]
        
        my $flesh = $toString->($entry);
        if (!utf8::valid($flesh)) {
            die __x("{reference}: invalid multibyte sequence\n",
                    reference => $entry->reference);
        }
    } else {
        # Convert.
        my $msgid = Locale::PO->dequote($entry->msgid);
        if (!__empty $msgid) {
            $cd->recode($msgid) 
                or $self->__conversionError($entry->reference, $cd);
            $entry->msgid($msgid);
        }
        
        my $msgid_plural = Locale::PO->dequote($entry->msgid_plural);
        if (!__empty $msgid_plural) {
            $cd->recode($msgid_plural) 
                or $self->__conversionError($entry->reference, $cd);
            $entry->msgid($msgid_plural);
        }
        
        my $msgstr = Locale::PO->dequote($entry->msgstr);
        if (!__empty $msgstr) {
            $cd->recode($msgstr) 
                or $self->__conversionError($entry->reference, $cd);
            $entry->msgid($msgstr);
        }
        
        my $msgstr_n = Locale::PO->dequote($entry->msgstr_n);
        if ($msgstr_n) {
            my $msgstr_0 = Locale::PO->dequote($msgstr_n->{0});
            $cd->recode($msgstr_0) 
                or $self->__conversionError($entry->reference, $cd);
            my $msgstr_1 = Locale::PO->dequote($msgstr_n->{1});
            $cd->recode($msgstr_1) 
                or $self->__conversionError($entry->reference, $cd);
            $entry->msgstr_n({
                0 => $msgstr_0,
                1 => $msgstr_1,
            })
        }
        
        my $comment = $entry->comment;
        $cd->recode($comment) 
            or $self->__conversionError($entry->reference, $cd);
        $entry->comment($comment);
    }

    return $self;        
}

sub options {
    shift->{__options};
}

sub option {
    my ($self, $key) = @_;

    return if !exists $self->{__options}->{$key};
    
    return $self->{__options}->{$key};
}

sub setOption {
    my ($self, $key, $value) = @_;

    $self->{__options}->{$key} = $value;

    return $self;
}

sub output {
    my ($self) = @_;
    
    if (!$self->{__run}) {
        require Carp;
        Carp::croak(__"Attempt to output from extractor before run");
    }
    
    if (!$self->{__po}) {
        require Carp;
        Carp::croak(__"No PO data");
    }
    
    return if !$self->{__po}->entries && !$self->{__options}->{force_po};

    my $options = $self->{__options};
    my $filename = $self->__outputFilename;

    open my $fh, '>', $filename
        or die __x("Error writing '{file}': {error}.\n",
                   file => $filename, error => $!);
    
    foreach my $entry ($self->{__po}->entries) {
        print $fh $entry->dump
            or die __x("Error writing '{file}': {error}.\n",
                       file => $filename, error => $!);
    }
    close $fh
        or die __x("Error writing '{file}': {error}.\n",
                   file => $filename, error => $!);
    
    return $self;
}

sub languageSpecificOptions {}

# In order to simplify the code in other languages, we allow returning
# a flat list instead of an array of arrays.  This wrapper checks the
# return value and converts it accordingly.
sub __languageSpecificOptions {
    my ($self) = @_;

    my @options = $self->languageSpecificOptions;
    return $options[0] if @options & 0x3;

    # Number of items is a multiple of 4.
    my @retval;
    while (@options) {
            push @retval, [splice @options, 0, 4];
    }

    return \@retval;
}

sub printLanguageSpecificUsage {
    my ($self) = @_;
  
    my $options = $self->__languageSpecificOptions;
    
    foreach my $optspec (@{$options || []}) {
        my ($optstring, $optvar,
            $usage, $description) = @$optspec;
        
        print "  $usage ";
        my $pos = 3 + length $usage;
        
        my @description = split /[ \x09-\x0d]+/, $description;
        my $lineno = 0;
        while (@description) {
            my $limit = $lineno ? 31 : 29;
            if ($pos < $limit) {
                print ' ' x ($limit - $pos);
                $pos = $limit;
            }
            
            while (@description) {
                my $word = shift @description;
                print " $word";
                $pos += 1 + length $word;
                if (@description && $pos > 77 - length $description[-1]) {
                    ++$lineno;
                    print "\n";
                    $pos = 0;
                    last;
                }
            }
        }
        print "\n";
    }
    
    return $self;
}

sub fileInformation {}

sub bugTrackingAddress {}

sub versionInformation {
    my ($self) = @_;
    
    my $package = ref $self;

    my $version;
    {
        ## no critic
        no strict 'refs';

        my $varname = "${package}::VERSION";
        $version = ${$varname};
    };

    $version = '' if !defined $version;
    
    $package =~ s/::/-/g;
    
    return __x('{program} ({package}) {version}
Please see the source for copyright information!
', program => $self->programName, package => $package, version => $version);
}

sub canExtractAll {
    return;
}

sub canKeywords {
    shift;
}

sub canFlags {
    shift;
}

sub needInputFiles {
    shift;
}

sub __readExcludeFiles {
    my ($self, $files) = @_;
    
    return $self if !$files;
    
    foreach my $file (@$files) {
       my $entries = Locale::PO->load_file_asarray($file)
        or die __x("error reading '{filename}': {error}!\n",
                   filename => $file, error => $!);
    
        foreach my $entry (@$entries) {
            my $msgid = $entry->msgid;
            next if __empty $msgid;
            
            my $ctx = $entry->msgctxt;
            $ctx = '' if __empty $ctx;
            
            $self->{__exclude}->{$msgid}->{$ctx} = $entry;
        }
    }
    
    return $self;
}

sub __promoteEntry {
    my ($self, $entry) = @_;
    
    if (!blessed $entry) {
        my %entry = %$entry;
        my $po_entry = Locale::PO->new;

        my $keyword = delete $entry{keyword};
        if (defined $keyword) {
            my $keywords = $self->keywords;
            if (exists $keywords->{$keyword}) {
                my $comment = $keywords->{$keyword}->comment;
                $entry{automatic} = $comment if !__empty $comment;

                my $flags = $self->flags;
                my $sg_arg = $keywords->{$keyword}->singular;
                my $pl_arg = $keywords->{$keyword}->plural || 0;
                foreach my $flag (@$flags) {
                    next if $keyword ne $flag->function;
                    next if $flag->arg != $sg_arg && $flag->arg != $pl_arg;
                    my $flag_name = $flag->flag;
                    $flag_name = 'no-' . $flag_name if $flag->no;
                    $po_entry->add_flag($flag_name);
                }
            }
        }

        my $flags = delete $entry{flags};
        if (defined $flags) {
            my @flags = split /[ \t\r\n]*,[ \t\r\n]*/, $flags;
            foreach my $flag (@flags) {
                $po_entry->add_flag($flag)
                    if !$po_entry->has_flag($flag);
            }
        }

        foreach my $method (keys %entry) {
            eval { $po_entry->$method($entry{$method}) };
            warn __x("error calling method '{method}' with value '{value}'"
                     . " on Locale::PO instance: {error}.\n",
                     method => $method, value => $entry{$method},
                     error => $@) if $@;
        }

        $entry = $po_entry;
    }

    return $entry;
}

sub __conversionError {
    my ($self, $reference, $cd) = @_;
    
    die __x("{reference}: {conversion_error}\n",
            reference => $reference,
            conversion_error => $cd->getError);
}

sub __outputFilename {
    my ($self) = @_;
    
    my $options = $self->{__options};
    if (exists $options->{output}) {
        if (File::Spec->file_name_is_absolute($options->{output})
            || '-' eq $options->{output}) {
            return $options->{output}; 
        } else {
            return File::Spec->catfile($options->{output_dir},
                                       $options->{output})
        }
    } elsif ('-' eq $options->{default_domain}) {
        return '-';
    } else {
        return File::Spec->catfile($options->{output_dir}, 
                                   $options->{default_domain} . '.po');
    }
    
    # NOT REACHED!
}
sub __poHeader {
    my ($self) = @_;

    my $options = $self->{__options};
    
    my $user_info;
    if ($options->{foreign_user}) {
        $user_info = <<EOF;
This file is put in the public domain.
EOF
    } else {
        my $copyright = $options->{copyright_holder};
        $copyright = "THE PACKAGE'S COPYRIGHT HOLDER" if !defined $copyright;
        
        $user_info = <<EOF;
Copyright (C) YEAR $copyright
This file is distributed under the same license as the PACKAGE package.
EOF
    }
    chomp $user_info;
    
    my $entry = Locale::PO->new;
    $entry->fuzzy(1);
    $entry->comment(<<EOF);
SOME DESCRIPTIVE TITLE.
$user_info
FIRST AUTHOR <EMAIL\@ADDRESS>, YEAR.
EOF
    $entry->msgid('');

    my @fields;
    
    my $package_name = $options->{package_name};
    if (defined $package_name) {
        my $package_version = $options->{package_version};
        $package_name .= ' ' . $package_version 
            if defined $package_version && length $package_version; 
    } else {
        $package_name = 'PACKAGE VERSION'
    }
    
    push @fields, "Project-Id-Version: $package_name";

    my $msgid_bugs_address = $options->{msgid_bugs_address};
    $msgid_bugs_address = '' if !defined $msgid_bugs_address;
    push @fields, "Report-Msgid-Bugs-To: $msgid_bugs_address";    
    
    push @fields, 'Last-Translator: FULL NAME <EMAIL@ADDRESS>';
    push @fields, 'Language-Team: LANGUAGE <LL@li.org>';
    push @fields, 'Language: ';
    push @fields, 'MIME-Version: ';
    # We always write utf-8.
    push @fields, 'Content-Type: text/plain; charset=UTF-8';
    push @fields, 'Content-Transfer-Encoding: 8bit';
    
    $entry->msgstr(join "\n", @fields);    
    return $entry;
}

sub __getEntriesFromFile {
    my ($self, $filename) = @_;

    open my $fh, '<', $filename
        or die __x("Error reading '{filename}': {error}!\n",
                   filename => $filename, error => $!);
    
    my @entries;
    my $chunk = '';
    my $last_lineno = 1;
    while (my $line = <$fh>) {
        if ($line =~ /^[\x09-\x0d ]*$/) {
            if (length $chunk) {
                my $entry = Locale::PO->new;
                chomp $chunk;
                $entry->msgid($chunk);
                $entry->reference("$filename:$last_lineno");
                push @entries, $entry;
            }
            $last_lineno = $. + 1;
            $chunk = '';
        } else {
            $chunk .= $line;
        }
    }
    
    if (length $chunk) {
        my $entry = Locale::PO->new;
        $entry->msgid($chunk);
        chomp $chunk;
        $entry->reference("$filename:$last_lineno");
        push @entries, $entry;
    }

    return @entries;
}

sub __readFilesFrom {
    my ($self, $list) = @_;
    
    my %seen;
    my @files;
    foreach my $file (@{$self->{__files}}) {
        my $canonical = File::Spec->canonpath($file);
        push @files, $file if !$seen{$canonical}++;
    }
    
    # This matches the format expected by GNU xgettext.  Lines where the
    # first non-whitespace character is a hash sign, are ignored.  So are
    # empty lines (after whitespace stripping).  All other lines are treated
    # as filenames with trailing (not leading!) space stripped off.
    foreach my $potfile (@$list) {
        open my $fh, '<', $potfile
            or die __x("Error opening '{file}': {error}!\n",
                       file => $potfile, error => $!);
        while (my $file = <$fh>) {
            next if $file =~ /^[ \x09-\x0d]*#/;
            $file =~ s/[ \x09-\x0d]+$//;
            next if !length $file;
            
            my $canonical = File::Spec->canonpath($file);
            next if $seen{$canonical}++;

            push @files, $file;
        }
    }
    
    $self->{__files} = \@files;
    
    return $self;
}

sub __getOptions {
    my ($self, $argv) = @_;
    
    my %options;
    
    my $lang_options = $self->__languageSpecificOptions;
    my %lang_options;
    
    foreach my $optspec (@$lang_options) {
        my ($optstring, $optvar,
            $usage, $description) = @$optspec;
        $lang_options{$optstring} = \$options{$optvar};
    }
    
    Getopt::Long::Configure('bundling');
    $SIG{__WARN__} = sub {
        $SIG{__WARN__} = 'DEFAULT';
        die shift;
    };
    GetOptionsFromArray($argv,
        # Are always overridden by standard options.
        %lang_options,
        
        # Input file location:
        'f|files-from=s@' => \$options{files_from},
        'D|directory=s@' => \$options{directory},

        # Output file location:
        'd|default-domain=s' => \$options{default_domain},
        'o|output=s' => \$options{output},
        'p|output-dir=s' => \$options{output_dir},

        # Input file interpretation.
        'from-code=s' => \$options{from_code},
       
        # Operation mode:
        'j|join-existing' => \$options{join_existing},
        
        # We allow multiple files.
        'x|exclude-file=s@' => \$options{exclude_file},
        'c|add-comments:s@' => \$options{add_comments},

        # Language specific options:
        'a|extract-all' => \$options{extract_all},
        'k|keyword:s@' => \$options{keyword},
        'flag:s@' => \$options{flag},
         
        # Output details:
        'force-po' => \$options{force_po},
        'no-location' => \$options{no_location},
        'n|add-location' => \$options{add_location},
        's|sort-output' => \$options{sort_output},
        'F|sort-by-file' => \$options{sort_by_file},
        'omit-header' => \$options{omit_header},
        'copyright-holder=s' => \$options{copyright_holder},
        'foreign-user' => \$options{foreign_user},
        'package_name=s' => \$options{package_name},
        'package_version=s' => \$options{package_version},
        'msgid-bugs-address=s' => \$options{msgid_bugs_address},
        'm|msgstr-prefix:s' => \$options{msgid_str_prefix},
        'M|msgstr-suffix:s' => \$options{msgid_str_suffix},

        # Informative output.
        'h|help' => \$options{help},
        'V|version' => \$options{version},
    );
    $SIG{__WARN__} = 'DEFAULT';
    
    foreach my $key (keys %options) {
        delete $options{$key} if !defined $options{$key};
    }
    
    return %options;   
}

sub __setKeywords {
    my ($self, $options) = @_;

    my $defaults = $self->defaultKeywords || [];
    
    my $keywords = {};
    foreach my $option (@$defaults, @$options) {
        if ('' eq $option) {
            $keywords = {};
            next;
        }

        my $keyword;
        if (ref $option) {
            $keyword = $option;
        } else {
            $keyword = Locale::XGettext::Util::Keyword->newFromString($option);
        }
        $keywords->{$keyword->function} = $keyword;
    }

    return $keywords;
}

sub __setFlags {
    my ($self, $options) = @_;
    
    my @defaults = @{$self->defaultFlags};

    my %flags;
    my @flags;

    foreach my $spec (@defaults, @$options) {
        my $obj = Locale::XGettext::Util::Flag->newFromString($spec)
            or die __x("A --flag argument doesn't have the"
                       . " <keyword>:<argnum>:[pass-]<flag> syntax: {flag}",
                       $spec);
        my $function = $obj->function;
        my $arg = $obj->arg;
        my $flag = $obj->flag;

        # First one wins.
        next if $flags{$function}->{$flag}->{$arg};

        $flags{$function}->{$flag}->{$arg} = $obj;
        push @flags, $obj;
    }
    
    return \@flags;
}

sub programName { $0 }

sub __displayUsage {
    my ($self) = @_;
    
    if ($self->needInputFiles) {
        print __x("Usage: {program} [OPTION] [INPUTFILE]...\n",
                  program => $self->programName);
        print "\n";
    
        print __(<<EOF);
Extract translatable strings from given input files.
EOF
    } else {
        print __x("Usage: {program} [OPTION]\n", program => $self->programName);
        print "\n";
    
        print __(<<EOF);
Extract translatable strings.
EOF
    }
    
    if (defined $self->fileInformation) {
        print "\n";
        my $description = $self->fileInformation;
        chomp $description;
        print "$description\n";
    }

    print "\n";
    
    print __(<<EOF);
Mandatory arguments to long options are mandatory for short options too.
Similarly for optional arguments.
EOF

    print "\n";

        print __(<<EOF);
Input file location:
EOF

    print __(<<EOF);
  INPUTFILE ...               input files
EOF

    print __(<<EOF);
  -f, --files-from=FILE       get list of input files from FILE\
EOF

    print __(<<EOF);
  -D, --directory=DIRECTORY   add DIRECTORY to list for input files search
EOF

    printf __(<<EOF);
If input file is -, standard input is read.
EOF

    print "\n";
    
    printf __(<<EOF);
Output file location:
EOF

    printf __(<<EOF);
  -d, --default-domain=NAME   use NAME.po for output (instead of messages.po)
EOF

    print __(<<EOF);
  -o, --output=FILE           write output to specified file
EOF

    print __(<<EOF);
  -p, --output-dir=DIR        output files will be placed in directory DIR
EOF

    print __(<<EOF);
If output file is -, output is written to standard output.
EOF

    print "\n";

    print __(<<EOF);
Input file interpretation:
EOF

    print __(<<EOF);
      --from-code=NAME        encoding of input files
EOF
    print __(<<EOF);
By default the input files are assumed to be in ASCII.
EOF

    printf "\n";

    print __(<<EOF);
Operation mode:
EOF

    print __(<<EOF);
  -j, --join-existing         join messages with existing file
EOF

    print __(<<EOF);
  -x, --exclude-file=FILE.po  entries from FILE.po are not extracted
EOF

    print __(<<EOF);
  -cTAG, --add-comments=TAG   place comment blocks starting with TAG and
                                preceding keyword lines in output file
  -c, --add-comments          place all comment blocks preceding keyword lines
                                in output file
EOF

    print "\n";

    print __(<<EOF);
Language specific options:
EOF

    if ($self->canExtractAll) {
        print __(<<EOF);
  -a, --extract-all           extract all strings
EOF
    }

    if ($self->canKeywords) {
        print __(<<EOF);
  -kWORD, --keyword=WORD      look for WORD as an additional keyword
  -k, --keyword               do not to use default keywords"));
      --flag=WORD:ARG:FLAG    additional flag for strings inside the argument
                              number ARG of keyword WORD
EOF
    }

    $self->printLanguageSpecificUsage;

    print "\n";

    print __(<<EOF);
Output details:
EOF

    print __(<<EOF);
      --force-po              write PO file even if empty
EOF

    print __(<<EOF);
      --no-location           do not write '#: filename:line' lines
EOF

    print __(<<EOF);
  -n, --add-location          generate '#: filename:line' lines (default)
EOF

    print __(<<EOF);
  -s, --sort-output           generate sorted output
EOF

    print __(<<EOF);
  -F, --sort-by-file          sort output by file location
EOF

    print __(<<EOF);
      --omit-header           don't write header with 'msgid ""' entry
EOF

    print __(<<EOF);
      --copyright-holder=STRING  set copyright holder in output
EOF

    print __(<<EOF);
      --foreign-user          omit FSF copyright in output for foreign user
EOF

    print __(<<EOF);
      --package-name=PACKAGE  set package name in output
EOF

    print __(<<EOF);
      --package-version=VERSION  set package version in output
EOF

    print __(<<EOF);
      --msgid-bugs-address=EMAIL\@ADDRESS  set report address for msgid bugs
EOF

    print __(<<EOF);
  -m[STRING], --msgstr-prefix[=STRING]  use STRING or "" as prefix for msgstr
                                values
EOF

    print __(<<EOF);
  -M[STRING], --msgstr-suffix[=STRING]  use STRING or "" as suffix for msgstr
                                values
EOF

    printf "\n";

    print __(<<EOF);
Informative output:
EOF

    print __(<<EOF);
  -h, --help                  display this help and exit
EOF

    print __(<<EOF);
  -V, --version               output version information and exit
EOF

    my $url = $self->bugTrackingAddress;

    printf "\n";

    if (defined $url) {
        # TRANSLATORS: The placeholder indicates the bug-reporting address
        # for this package.  Please add _another line_ saying
        # "Report translation bugs to <...>\n" with the address for translation
        # bugs (typically your translation team's web or email address).
        print __x("Report bugs at <{URL}>!\n", URL => $url);
    }

    exit 0;
}

sub __usageError {
    my ($self, $message) = @_;

    if ($message) {
        $message =~ s/\s+$//;
        $message = __x("{program_name}: {error}\n",
                       program_name => $self->programName, error => $message);
    } else {
        $message = '';
    }
    
    die $message . __x("Try '{program_name} --help' for more information!\n",
                       program_name => $self->programName);
}

1;