# ------------------------------------------------------------------ # Petal::I18N - Independant I18N processing # ------------------------------------------------------------------ package Petal::I18N; use MKDoc::XML::TreeBuilder; use MKDoc::XML::TreePrinter; use Petal::Hash::String; use warnings; use strict; our $Namespace = "http://xml.zope.org/namespaces/i18n"; our $Prefix = 'i18n'; our $Domain = 'default'; sub process { my $class = shift; my $data = shift; local $Namespace = $Namespace; local $Prefix = $Prefix; local $Domain = $Domain; my @nodes = MKDoc::XML::TreeBuilder->process_data ($data); for (@nodes) { $class->_process ($_) } return MKDoc::XML::TreePrinter->process (@nodes); } sub _process { my $class = shift; my $tree = shift; return unless (ref $tree); local $Prefix = $Prefix; local $Domain = $Domain; # process the I18N namespace foreach my $key (keys %{$tree}) { my $value = $tree->{$key}; if ($value eq $Namespace) { next unless ($key =~ /^xmlns\:/); delete $tree->{$key}; $Prefix = $key; $Prefix =~ s/^xmlns\://; } } # set the current i18n:domain $Domain = delete $tree->{"$Prefix:domain"} || $Domain; my $tag = $tree->{_tag}; my $attr = { map { /^_/ ? () : ( $_ => $tree->{$_} ) } keys %{$tree} }; return if ($tag eq '~comment' or $tag eq '~pi' or $tag eq '~declaration'); # replace attributes with their respective translations $tree->{"$Prefix:attributes"} && do { my $attributes = $tree->{"$Prefix:attributes"}; $attributes =~ s/\s*;\s*$//; $attributes =~ s/^\s*//; my @attributes = split /\s*\;\s*/, $attributes; foreach my $attribute (@attributes) { # if we have i18n:attributes="alt alt_text", then the # attribute name is 'alt' and the # translate_id is 'alt_text' my ($attribute_name, $translate_id); if ($attribute =~ /\s/) { ($attribute_name, $translate_id) = split /\s+/, $attribute, 2; } # otherwise, if we have i18n:attributes="alt", then the # attribute name is 'alt' and the # translate_id is $tree->{'alt'} else { $attribute_name = $attribute; $translate_id = _canonicalize ( $tree->{$attribute_name} ); } # the default value if maketext() fails should be the current # value of the attribute my $default_value = $tree->{$attribute_name}; # the value to replace the attribute with should be either the # translation, or the default value if maketext() failed. my $value = eval { $Petal::TranslationService->maketext ($translate_id) } || $default_value; # if maketext() failed, let's know why. $@ && warn $@; # set the (hopefully) translated value $tree->{$attribute_name} = $value; } }; # replace content with its translation exists $tree->{"$Prefix:translate"} && do { my ($translate_id); # if we have $Domain:translate="something", # then the translate_id is 'something' if (defined $tree->{"$Prefix:translate"} and $tree->{"$Prefix:translate"} ne '') { $translate_id = $tree->{"$Prefix:translate"}; } # otherwise, the translate_id has to be computed # from the contents of this node, so that #
Hello, David, how are you?
# becomes 'Hello, ${user}, how are you?' else { $translate_id = _canonicalize ( _extract_content_string ($tree) ); } # the default value if maketext() fails should be the current # value of the attribute my $default_value = _canonicalize ( _extract_content_string ($tree) ); # the value to replace the content with should be either the # translation, or the default value if maketext() failed. my $value = eval { $Petal::TranslationService->maketext ($translate_id) } || $default_value; # now, $value is supposed to have the translated string, which looks like # 'Bonjour, ${user}, comment allez-vous?'. We need to turn this back into # a tree structure. my %named_nodes = _extract_named_nodes ($tree); my @tokens = @{Petal::Hash::String->_tokenize (\$value)}; my @res = map { ($_ =~ /$Petal::Hash::String::TOKEN_RE/gsm) ? do { s/^\$//; s/^\{//; s/\}$//; $named_nodes{$_}; } : do { s/\\(.)/$1/gsm; $_; }; } @tokens; $tree->{_content} = \@res; }; # I know, I know, the I18N namespace processing is a bit broken... # It should suffice for now. delete $tree->{"$Prefix:attributes"}; delete $tree->{"$Prefix:translate"}; delete $tree->{"$Prefix:name"}; # Do the same i18n thing with child nodes, recursively. # for some reason it always makes me think of roller coasters. # Yeeeeeeee! defined $tree->{_content} and do { for (@{$tree->{_content}}) { $class->_process ($_) } }; } sub _canonicalize { my $string = shift; return '' unless (defined $string); $string =~ s/\s+/ /gsm; $string =~ s/^ //; $string =~ s/ $//; return $string; } sub _extract_named_nodes { my $tree = shift; my @nodes = (); foreach my $node (@{$tree->{_content}}) { ref $node || next; push @nodes, $node; } my %nodes = (); my $count = 0; foreach my $node (@nodes) { $count++; my $name = $node->{"$Prefix:name"} || $count; $nodes{$name} = $node; } return %nodes; } sub _extract_content_string { my $tree = shift; my @res = (); my $count = 0; foreach my $node (@{$tree->{_content}}) { ref $node or do { push @res, $node; next; }; $count++; my $name = $node->{"$Prefix:name"} || $count; push @res, '${' . $name . '}'; } return join '', @res; } 1; __END__ =head1 NAME Petal::I18N - Attempt at implementing ZPT I18N for Petal =head1 SYNOPSIS in your Perl code: use Petal; use Petal::TranslationService::Gettext; my $translation_service = new Petal::TranslationService::Gettext ( locale_dir => '/path/to/my/app/locale', target_lang => gimme_target_lang(), ); my $template = new Petal ( file => 'example.html', translation_service => $translation_service ); # we want to internationalize to the h4x0rz 31337 l4nGu4g3z. w00t! my $translation_service = Petal::TranslationService::h4x0r->new(); my $template = new Petal ( file => 'silly_example.xhtml', translation_service => $ts, ); print $template->process (); =head1 I18N Howto =head2 Preparing your templates: Say your un-internationalized template looks like this: the logo of our organisation

Hello, Joe.

How are you today?

You need to markup your template according to the ZPT I18N specification, which can be found at http://dev.zope.org/Wikis/DevSite/Projects/ComponentArchitecture/ZPTInternationalizationSupport the logo of our organisation

Hello, Joe.

How are you today?

=head2 Extracting I18N strings: Once your templates are marked up properly, you will need to use a tool to extract the I18N strings into .pot (po template) files. To my knowledge you can use i18ndude (standalone python executable), i18nextract.py (part of Zope 3), or L. I use i18ndude to find strings which are not marked up properly with i18n:translate attributes and I18NFool for extracting strings and managing .po files. Assuming you're using i18nfool: mkdir -p /path/to/my/app/locale cd /path/to/my/app/locale i18nfool-extract /path/to/my/template/example.html mkdir en mkdir fr mkdir es i18nfool-update Then you translate the .po files into their respective target languages. When that's done, you type: cd /path/to/my/app/locale i18nfool-build And it builds all the .mo files. =head2 Making your application use a Gettext translation service: Previously you might have had: use Petal; # lotsa code here my $template = Petal->new ('example.html'); This needs to become: use Petal; use Petal::TranslationService::Gettext; # lotsa code here my $template = Petal->new ('example.html'); $template->{translation_service} = Petal::TranslationService::Gettext->new ( locale_dir => '/path/to/my/app/locale', target_lang => gimme_language_code(), ); Where gimme_language_code() returns a language code depending on LC_LANG, content-negotiation, config-file, or whatever mechanism you are using to decide which language is desired. =head2 And then? And then that's it! Your application should be easily internationalizable. There are a few traps / gotchas thought, which are described below. =head1 BUGS, TRAPS, GOTCHAS and other niceties =head2 Translation Phase The translation step takes place ONLY ONCE THE TEMPLATE HAS BEEN PROCESSED. So if you have:

Hello, Joe

It most likely will not work because the tal:replace would remove the tag and also the i18n:name in the process. This means that instead of receiving something such as: Hello, ${user_login} The translation service would receive: Hello, Fred Flintstone Or Hello, Joe SixPack etc. To fix this issue, use tal:content instead of tal:replace and leave the span and its i18n:name attribute. =head2 Character sets I haven't worried too much about them (yet) so if you run into trouble join the Petal mailing list and we'll try to fix any issues together. =head2 Limitations At the moment, L supports the following constructs: =over 4 =item xmlns:i18n="http://xml.zope.org/namespaces/i18n" =item i18n:translate =item i18n:domain =item i18n:name =item i18n:attribute =back It does *NOT* (well, not yet) support i18n:source, i18n:target or i18n:data. =cut