]> git.ipfire.org Git - thirdparty/bugzilla.git/commitdiff
Bug 374227: Create a system for localizing basic installation strings
authormkanat%bugzilla.org <>
Sat, 17 Mar 2007 21:13:34 +0000 (21:13 +0000)
committermkanat%bugzilla.org <>
Sat, 17 Mar 2007 21:13:34 +0000 (21:13 +0000)
Patch By Max Kanat-Alexander <mkanat@bugzilla.org> (module owner) a=mkanat

Bugzilla/Install/Util.pm
checksetup.pl
template/en/default/setup/strings.txt.pl [new file with mode: 0644]

index 16f8f3e046ee055d68b0180955150d5f05665b64..b02c435e501f1c00d4784fc93296ffac11202c62 100644 (file)
@@ -28,18 +28,20 @@ use strict;
 
 use Bugzilla::Constants;
 
+use File::Basename;
 use POSIX ();
+use Safe;
 
 use base qw(Exporter);
 our @EXPORT_OK = qw(
     display_version_and_os
     indicate_progress
+    install_string
     vers_cmp
 );
 
 sub display_version_and_os {
     # Display version information
-    printf "\n* This is Bugzilla " . BUGZILLA_VERSION . " on perl %vd\n", $^V;
     my @os_details = POSIX::uname;
     # 0 is the name of the OS, 2 is the major version,
     my $os_name = $os_details[0] . ' ' . $os_details[2];
@@ -47,8 +49,12 @@ sub display_version_and_os {
         require Win32;
         $os_name = Win32::GetOSName();
     }
-    # 3 is the minor version.
-    print "* Running on $os_name $os_details[3]\n"
+    # $os_details[3] is the minor version.
+    print install_string('version_and_os', { bz_ver   => BUGZILLA_VERSION,
+                                             perl_ver => sprintf('%vd', $^V),
+                                             os_name  => $os_name,
+                                             os_ver   => $os_details[3] })
+           . "\n";
 }
 
 sub indicate_progress {
@@ -63,6 +69,126 @@ sub indicate_progress {
     }
 }
 
+sub install_string {
+    my ($string_id, $vars) = @_;
+    _cache()->{template_include_path} ||= template_include_path();
+    my $path = _cache()->{template_include_path};
+    
+    my $string_template;
+    # Find the first set of templates that defines this string.
+    foreach my $dir (@$path) {
+        my $file = "$dir/setup/strings.txt.pl";
+        next unless -e $file;
+        my $safe = new Safe;
+        $safe->rdo($file);
+        my %strings = %{$safe->varglob('strings')};
+        $string_template = $strings{$string_id};
+        last if $string_template;
+    }
+    
+    die "No language defines the string '$string_id'" if !$string_template;
+
+    $vars ||= {};
+    my @replace_keys = keys %$vars;
+    foreach my $key (@replace_keys) {
+        my $replacement = $vars->{$key};
+        die "'$key' in '$string_id' is tainted: '$replacement'"
+            if is_tainted($replacement);
+        # We don't want people to start getting clever and inserting
+        # ##variable## into their values. So we check if any other
+        # key is listed in the *replacement* string, before doing
+        # the replacement. This is mostly to protect programmers from
+        # making mistakes.
+        if (grep($replacement =~ /##$key##/, @replace_keys)) {
+            die "Unsafe replacement for '$key' in '$string_id': '$replacement'";
+        }
+        $string_template =~ s/\Q##$key##\E/$replacement/g;
+    }
+    
+    return $string_template;
+}
+
+sub template_include_path {
+    my ($params) = @_;
+    $params ||= {};
+
+    # Basically, the way this works is that we have a list of languages
+    # that we *want*, and a list of languages that Bugzilla actually
+    # supports. The caller tells us what languages they want, by setting
+    # $ENV{HTTP_ACCEPT_LANGUAGE} or $params->{only_language}. The languages
+    # we support are those specified in $params->{use_languages}. Otherwise
+    # we support every language installed in the template/ directory.
+    
+    my @wanted;
+    if (defined $params->{only_language}) {
+        @wanted = ($params->{only_language});
+    }
+    else {
+        @wanted = _sort_accept_language($ENV{'HTTP_ACCEPT_LANGUAGE'} || '');
+    }
+    
+    my @supported;
+    if (defined $params->{use_languages}) {
+        @supported = $params->{use_languages};
+    }
+    else {
+        my @dirs = glob(bz_locations()->{'templatedir'} . "/*");
+        @dirs = map(basename($_), @dirs);
+        @supported = grep($_ ne 'CVS', @dirs);
+    }
+    
+    my @usedlanguages;
+    foreach my $wanted (@wanted) {
+        # If we support the language we want, or *any version* of
+        # the language we want, it gets pushed into @usedlanguages.
+        #
+        # Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and
+        # 'en-uk', but not the other way around. (This is unfortunately
+        # not very clearly stated in those RFC; see comment just over 14.5
+        # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4)
+        if(my @found = grep /^\Q$wanted\E(-.+)?$/i, @supported) {
+            push (@usedlanguages, @found);
+        }
+    }
+
+    # If we didn't want *any* of the languages we support, just use all
+    # of the languages we said we support, in the order they were specified.
+    # This is only done when you ask for a certain set of languages, because
+    # otherwise @supported just came off the disk in alphabetical order,
+    # and it could give you de (German) when you speak English.
+    # (If @supported came off the disk, we fall back on English if no language
+    # is available--that happens below.)
+    if (!@usedlanguages && $params->{use_languages}) {
+        @usedlanguages = @supported;
+    }
+    
+    # We always include English at the bottom if it's not there, even if
+    # somebody removed it from use_languages.
+    if (!grep($_ eq 'en', @usedlanguages)) {
+        push(@usedlanguages, 'en');
+    }
+    
+    # Now, we add template directories in the order they will be searched:
+    
+    # First, we add extension template directories, because extension templates
+    # override standard templates. Extensions may be localized in the same way
+    # that Bugzilla templates are localized.
+    my @include_path;
+    my @extensions = glob(bz_locations()->{'extensionsdir'} . "/*");
+    foreach my $extension (@extensions) {
+        foreach my $lang (@usedlanguages) {
+            _add_language_set(\@include_path, $lang, "$extension/template");
+        }
+    }
+    
+    # Then, we add normal template directories, sorted by language.
+    foreach my $lang (@usedlanguages) {
+        _add_language_set(\@include_path, $lang);
+    }
+    
+    return \@include_path;
+}
+
 # This is taken straight from Sort::Versions 1.5, which is not included
 # with perl by default.
 sub vers_cmp {
@@ -106,6 +232,81 @@ sub vers_cmp {
     @A <=> @B;
 }
 
+######################
+# Helper Subroutines #
+######################
+
+# Used by template_include_path.
+sub _add_language_set {
+    my ($array, $lang, $templatedir) = @_;
+    
+    $templatedir ||= bz_locations()->{'templatedir'};
+    my @add = ("$templatedir/$lang/custom", "$templatedir/$lang/default");
+    
+    my $project = bz_locations->{'project'};
+    push(@add, "$templatedir/$lang/$project") if $project;
+    
+    foreach my $dir (@add) {
+        #if (-d $dir) {
+            trick_taint($dir);
+            push(@$array, $dir);
+        #}
+    }
+}
+
+# Make an ordered list out of a HTTP Accept-Language header (see RFC 2616, 14.4)
+# We ignore '*' and <language-range>;q=0
+# For languages with the same priority q the order remains unchanged.
+sub _sort_accept_language {
+    sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} }
+    my $accept_language = $_[0];
+
+    # clean up string.
+    $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g;
+    my @qlanguages;
+    my @languages;
+    foreach(split /,/, $accept_language) {
+        if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) {
+            my $lang   = $1;
+            my $qvalue = $2;
+            $qvalue = 1 if not defined $qvalue;
+            next if $qvalue == 0;
+            $qvalue = 1 if $qvalue > 1;
+            push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang});
+        }
+    }
+
+    return map($_->{'language'}, (sort sortQvalue @qlanguages));
+}
+
+
+# This is like request_cache, but it's used only by installation code
+# for setup.cgi and things like that.
+our $_cache = {};
+sub _cache {
+    if ($ENV{MOD_PERL}) {
+        require Apache2::RequestUtil;
+        return Apache2::RequestUtil->request->pnotes();
+    }
+    return $_cache;
+}
+
+###############################
+# Copied from Bugzilla::Util #
+##############################
+
+sub trick_taint {
+    require Carp;
+    Carp::confess("Undef to trick_taint") unless defined $_[0];
+    my $match = $_[0] =~ /^(.*)$/s;
+    $_[0] = $match ? $1 : undef;
+    return (defined($_[0]));
+}
+
+sub is_tainted {
+    return not eval { my $foo = join('',@_), kill 0; 1; };
+}
+
 __END__
 
 =head1 NAME
@@ -173,6 +374,52 @@ ten items. Defaults to 1 if not specified.
 
 =back
 
+=item C<install_string>
+
+=over
+
+=item B<Description>
+
+This is a very simple method of templating strings for installation.
+It should only be used by code that has to run before the Template Toolkit
+can be used. (See the comments at the top of the various L<Bugzilla::Install>
+modules to find out when it's safe to use Template Toolkit.)
+
+It pulls strings out of the F<strings.txt.pl> "template" and replaces
+any variable surrounded by double-hashes (##) with a value you specify.
+
+This allows for localization of strings used during installation.
+
+=item B<Example>
+
+Let's say your template string looks like this:
+
+ The ##animal## jumped over the ##plant##.
+Let's say that string is called 'animal_jump_plant'. So you call the function
+like this:
+
+ install_string('animal_jump_plant', { animal => 'fox', plant => 'tree' });
+
+That will output this:
+
+ The fox jumped over the tree.
+
+=item B<Params>
+
+=over
+
+=item C<$string_id> - The name of the string from F<strings.txt.pl>.
+
+=item C<$vars> - A hashref containing the replacement values for variables
+inside of the string.
+
+=back
+
+=item B<Returns>: The appropriate string, with variables replaced.
+
+=back
+
 =item C<vers_cmp>
 
 =over
index 5e684db0a78af7bd1b2e065c04e82b576ade40e0..aff7bb796a2a836b406e3b2d300ea798b70e8b7a 100755 (executable)
@@ -62,6 +62,10 @@ require 5.008001 if ON_WINDOWS; # for CGI 2.93 or higher
 # Live Code
 ######################################################################
 
+# When we're running at the command line, we need to pick the right
+# language before ever displaying any string.
+$ENV{'HTTP_ACCEPT_LANGUAGE'} ||= setlocale(LC_CTYPE);
+
 my %switch;
 GetOptions(\%switch, 'help|h|?', 'check-modules', 'no-templates|t',
                      'verbose|v|no-silent', 'make-admin=s');
@@ -116,10 +120,6 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
 Bugzilla->installation_mode(INSTALLATION_MODE_NON_INTERACTIVE) if $answers_file;
 Bugzilla->installation_answers($answers_file);
 
-# When we're running at the command line, we need to pick the right
-# language before ever creating a template object.
-$ENV{'HTTP_ACCEPT_LANGUAGE'} ||= setlocale(LC_CTYPE);
-
 ###########################################################################
 # Check and update --LOCAL-- configuration
 ###########################################################################
diff --git a/template/en/default/setup/strings.txt.pl b/template/en/default/setup/strings.txt.pl
new file mode 100644 (file)
index 0000000..0942bb0
--- /dev/null
@@ -0,0 +1,16 @@
+# This file contains a single hash named %strings, which is used by the
+# installation code to display strings before Template-Toolkit can safely
+# be loaded.
+#
+# Each string supports a very simple substitution system, where you can
+# have variables named like ##this## and they'll be replaced by the string
+# variable with that name.
+#
+# Please keep the strings in alphabetical order by their name.
+
+%strings = (
+    version_and_os => "* This is Bugzilla ##bz_ver## on perl ##perl_ver##\n"
+                      . "* Running on ##os_name## ##os_ver##",
+);
+
+1;