use strict; use warnings; package Locale::PO; use Carp; #use vars qw($VERSION @ISA @EXPORT @EXPORT_OK); #use locale; #require Exporter; #require AutoLoader; #@ISA = qw(Exporter AutoLoader); #@EXPORT = qw(); our $VERSION = '0.16'; # Preloaded methods go here. sub new { my $this = shift; my %options = @_; my $class = ref($this) || $this; my $self = {}; bless $self, $class; $self->msgid( $options{'-msgid'} ) if defined( $options{'-msgid'} ); $self->msgid_plural( $options{'-msgid_plural'} ) if defined( $options{'-msgid_plural'} ); $self->msgstr( $options{'-msgstr'} ) if defined( $options{'-msgstr'} ); $self->msgstr_n( $options{'-msgstr_n'} ) if defined( $options{'-msgstr_n'} ); $self->comment( $options{'-comment'} ) if defined( $options{'-comment'} ); $self->fuzzy( $options{'-fuzzy'} ) if defined( $options{'-fuzzy'} ); $self->automatic( $options{'-automatic'} ) if defined( $options{'-automatic'} ); $self->reference( $options{'-reference'} ) if defined( $options{'-reference'} ); $self->c_format(1) if defined( $options{'-c-format'} ); $self->c_format(1) if defined( $options{'-c_format'} ); $self->c_format(0) if defined( $options{'-no-c-format'} ); $self->c_format(0) if defined( $options{'-no_c_format'} ); return $self; } sub msgid { my $self = shift; @_ ? $self->{'msgid'} = $self->quote(shift) : $self->{'msgid'}; } sub msgid_plural { my $self = shift; @_ ? $self->{'msgid_plural'} = $self->quote(shift) : $self->{'msgid_plural'}; } sub msgstr { my $self = shift; @_ ? $self->{'msgstr'} = $self->quote(shift) : $self->{'msgstr'}; } sub msgstr_n { my $self = shift; if (@_) { my $hashref = shift; # check that we have a hashref. croak 'Argument to msgstr_n must be a hashref: { n => "string n", ... }.' unless ref($hashref) eq 'HASH'; # Check that the keys are all numbers. croak 'Keys to msgstr_n hashref must be numbers' if grep { m/\D/ } keys %$hashref; # Quote all the values in the hashref. $self->{'msgstr_n'}{$_} = $self->quote( $$hashref{$_} ) for keys %$hashref; } return $self->{'msgstr_n'}; } sub comment { my $self = shift; @_ ? $self->{'comment'} = shift: $self->{'comment'}; } sub automatic { my $self = shift; @_ ? $self->{'automatic'} = shift: $self->{'automatic'}; } sub reference { my $self = shift; @_ ? $self->{'reference'} = shift: $self->{'reference'}; } sub fuzzy { my $self = shift; @_ ? $self->{'fuzzy'} = shift: $self->{'fuzzy'}; } sub c_format { my $self = shift; @_ ? $self->{'c_format'} = shift: $self->{'c_format'}; } sub php_format { my $self = shift; @_ ? $self->{'php_format'} = shift: $self->{'php_format'}; } sub normalize_str { my $self = shift; my $string = shift; my $dequoted = $self->dequote($string); # This isn't quite perfect, but it's fast and easy if ( $dequoted =~ /(^|[^\\])(\\\\)*\\n./ ) { # Multiline my $output; my @lines; $output = '""' . "\n"; @lines = split( /\\n/, $dequoted, -1 ); my $lastline = pop @lines; # special treatment for this one foreach (@lines) { $output .= $self->quote("$_\\n") . "\n"; } $output .= $self->quote($lastline) . "\n" if $lastline ne ""; return $output; } else { # Single line return "$string\n"; } } sub dump { my $self = shift; my $dump; $dump = $self->dump_multi_comment( $self->comment, "# " ) if ( $self->comment ); $dump .= $self->dump_multi_comment( $self->automatic, "#. " ) if ( $self->automatic ); $dump .= $self->dump_multi_comment( $self->reference, "#: " ) if ( $self->reference ); my $flags = ''; $flags .= ", fuzzy" if $self->fuzzy; $flags .= ", c-format" if ( defined( $self->c_format ) and $self->c_format ); $flags .= ", no-c-format" if ( defined( $self->c_format ) and !$self->c_format ); $flags .= ", php-format" if ( defined( $self->php_format ) and $self->php_format ); $dump .= "#$flags\n" if length $flags; $dump .= "msgid " . $self->normalize_str( $self->msgid ); $dump .= "msgid_plural " . $self->normalize_str( $self->msgid_plural ) if $self->msgid_plural; $dump .= "msgstr " . $self->normalize_str( $self->msgstr ) if $self->msgstr; if ( my $msgstr_n = $self->msgstr_n ) { $dump .= "msgstr[$_] " . $self->normalize_str( $$msgstr_n{$_} ) for sort { $a <=> $b } keys %$msgstr_n; } $dump .= "\n"; return $dump; } sub dump_multi_comment { my $self = shift; my $comment = shift; my $leader = shift; my $chopped = $leader; chop($chopped); my $result = $leader . $comment; $result =~ s/\n/\n$leader/g; $result =~ s/^$leader$/$chopped/gm; $result .= "\n"; return $result; } # Quote a string properly sub quote { my $self = shift; my $string = shift; $string =~ s/"/\\"/g; return "\"$string\""; } sub dequote { my $self = shift; my $string = shift; $string =~ s/^"(.*)"/$1/; $string =~ s/\\"/"/g; return $string; } sub save_file_fromarray { my $self = shift; $self->save_file( @_, 0 ); } sub save_file_fromhash { my $self = shift; $self->save_file( @_, 1 ); } sub save_file { my $self = shift; my $file = shift; my $entries = shift; my $ashash = shift; open( OUT, ">$file" ) or return undef; if ($ashash) { foreach ( sort keys %$entries ) { print OUT $entries->{$_}->dump; } } else { foreach (@$entries) { print OUT $_->dump; } } close OUT; } sub load_file_asarray { my $self = shift; $self->load_file( $_[0], 0 ); } sub load_file_ashash { my $self = shift; $self->load_file( $_[0], 1 ); } sub load_file { my $self = shift; my $file = shift; my $ashash = shift; my ( @entries, %entries ); my $po; my %buffer; my $last_buffer; open( IN, "<$file" ) or return undef; while () { chop; if (/^$/) { # Empty line. End of an entry. if ( defined($po) ) { $po->msgid( $buffer{msgid} ) if defined $buffer{msgid}; $po->msgid_plural( $buffer{msgid_plural} ) if defined $buffer{msgid_plural}; $po->msgstr( $buffer{msgstr} ) if defined $buffer{msgstr}; $po->msgstr_n( $buffer{msgstr_n} ) if defined $buffer{msgstr_n}; if ($ashash) { my $key = $po->msgid; if ( defined( $entries{$key} ) ) { # Prefer translated ones. $entries{ $po->msgid } = $po if $entries{$key}->msgstr !~ /\w/; } else { # No previous entry $entries{ $po->msgid } = $po; } } else { push( @entries, $po ); } undef $po; undef $last_buffer; %buffer = (); } } elsif ( /^# (.*)/ or /^#()$/ ) { # Translator comments $po = new Locale::PO unless defined($po); if ( defined( $po->comment ) ) { $po->comment( $po->comment . "\n$1" ); } else { $po->comment($1); } } elsif (/^#\. (.*)/) { # Automatic comments $po = new Locale::PO unless defined($po); if ( defined( $po->automatic ) ) { $po->automatic( $po->automatic . "\n$1" ); } else { $po->automatic($1); } } elsif (/^#: (.*)/) { # reference $po = new Locale::PO unless defined($po); if ( defined( $po->reference ) ) { $po->reference( $po->reference . "\n$1" ); } else { $po->reference($1); } } elsif (/^#, (.*)/) { # flags my $flags = $1; $po = new Locale::PO unless defined($po); $po->fuzzy(1) if $flags =~ /fuzzy/i; $po->c_format(1) if $flags =~ /c-format/i; $po->c_format(0) if $flags =~ /no-c-format/i; $po->php_format(1) if $flags =~ /php-format/i; } elsif (/^msgid (.*)/) { $po = new Locale::PO unless defined($po); $buffer{msgid} = $self->dequote($1); $last_buffer = \$buffer{msgid}; } elsif (/^msgid_plural (.*)/) { $po = new Locale::PO unless defined($po); $buffer{msgid_plural} = $self->dequote($1); $last_buffer .= \$buffer{msgid_plural}; } elsif (/^msgstr (.*)/) { # translated string $buffer{msgstr} = $self->dequote($1); $last_buffer = \$buffer{msgstr}; } elsif (/^msgstr\[(\d+)\] (.*)/) { # translated string $buffer{msgstr_n}{$1} = $self->dequote($2); $last_buffer = \$buffer{msgstr_n}{$1}; } elsif (/^"/) { # contined string $$last_buffer .= $self->dequote($_); } else { warn "Strange line in $file: $_\n"; } } if ( defined($po) ) { $po->msgid( $buffer{msgid} ) if defined $buffer{msgid}; $po->msgid_plural( $buffer{msgid_plural} ) if defined $buffer{msgid_plural}; $po->msgstr( $buffer{msgstr} ) if defined $buffer{msgstr}; $po->msgstr_n( $buffer{msgstr_n} ) if defined $buffer{msgstr_n}; $entries{ $po->msgid } = $po if $ashash; push( @entries, $po ) unless $ashash; } close IN; return ( $ashash ? \%entries : \@entries ); } # Autoload methods go after =cut, and are processed by the autosplit program. 1; __END__ # Below is the stub of documentation for your module. You better edit it! =head1 NAME Locale::PO - Perl module for manipulating .po entries from GNU gettext =head1 SYNOPSIS use Locale::PO; $po = new Locale::PO([-option=>value,...]) [$string =] $po->msgid([new string]); [$string =] $po->msgstr([new string]); [$string =] $po->comment([new string]); [$string =] $po->automatic([new string]); [$string =] $po->reference([new string]); [$value =] $po->fuzzy([value]); [$value =] $po->c_format([value]); print $po->dump; $quoted_string = $po->quote($string); $string = $po->dequote($quoted_string); $aref = Locale::PO->load_file_asarray(); $href = Locale::PO->load_file_ashash(); Locale::PO->save_file_fromarray(,$aref); Locale::PO->save_file_fromhash(,$href); =head1 DESCRIPTION This module simplifies management of GNU gettext .po files and is an alternative to using emacs po-mode. It provides an object-oriented interface in which each entry in a .po file is a Locale::PO object. =head1 METHODS =over 4 =item new Create a new Locale::PO object to represent a po entry. You can optionally set the attributes of the entry by passing a list/hash of the form: -option=>value, -option=>value, etc. Where options are msgid, msgstr, comment, automatic, reference, fuzzy, and c-format. See accessor methods below. To generate a po file header, add an entry with an empty msgid, like this: $po = new Locale::PO(-msgid=>'', -msgstr=> "Project-Id-Version: PACKAGE VERSION\\n" . "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" . "Last-Translator: FULL NAME \\n" . "Language-Team: LANGUAGE \\n" . "MIME-Version: 1.0\\n" . "Content-Type: text/plain; charset=CHARSET\\n" . "Content-Transfer-Encoding: ENCODING\\n"); =item msgid Set or get the untranslated string from the object. =item msgid_plural Set or get the untranslated plural string from the object. =item msgstr Set or get the translated string from the object. =item msgstr_n Get or set the translations if there are purals involved. Takes and returns a hashref where the keys are the 'N' case and the values are the strings. eg: $po->msgstr_n( { 0 => 'found %d plural translations', 1 => 'found %d singular translation', } ); =item comment Set or get translator comments from the object. =item automatic Set or get automatic comments from the object (inserted by emacs po-mode or xgettext). =item reference Set or get reference marking comments from the object (inserted by emacs po-mode or gettext). =item fuzzy Set or get the fuzzy flag on the object ("check this translation"). When setting, use 1 to turn on fuzzy, and 0 to turn it off. =item c_format Set or get the c-format or no-c-format flag on the object. This can take 3 values: 1 implies c-format, 0 implies no-c-format, and blank or undefined implies neither. =item dump Returns the entry as a string, suitable for output to a po file. =item quote Applies po quotation rules to a string, and returns the quoted string. The quoted string will have all existing double-quote characters escaped by backslashes, and will be enclosed in double quotes. =item dequote Returns a quoted po string to its natural form. =item load_file_asarray Given the filename of a po-file, reads the file and returns a reference to a list of Locale::PO objects corresponding to the contents of the file, in the same order. =item load_file_ashash Given the filename of a po-file, reads the file and returns a reference to a hash of Locale::PO objects corresponding to the contents of the file. The hash keys are the untranslated strings, so this is a cheap way to remove duplicates. The method will prefer to keep entries that have been translated. =item save_file_fromarray Given a filename and a reference to a list of Locale::PO objects, saves those objects to the file, creating a po-file. =item save_file_fromhash Given a filename and a reference to a hash of Locale::PO objects, saves those objects to the file, creating a po-file. The entries are sorted alphabetically by untranslated string. =back =head1 AUTHOR Alan Schwartz, alansz@pennmush.org =head1 BUGS If you load_file then save_file, the output file may have slight cosmetic differences from the input file (an extra blank line here or there). =head1 SEE ALSO xgettext(1). =cut