Cees Hek provided me with a solution using H::T's filter. I think Sam
suggested a filter first but, I didn't understand. (I would'nt have figured
this out without seeing the sample Cees gave me.) It involves passing the
language value to H::T's load_tmpl method. Then override H::T's  _cache_key
method to add the language value to the key H::T uses to cache the template.
This lets you load a language-neutral template, update it for a language,
and H::T will cache it uniquely by template-name and language.

Another advantage is that I can generate all my pull-down (select/option)
lists *in the filter* and they become permanent parts of the cached
template -- not reevaluated with each output. When I create the string of
select/option values, I add a "<TMPL_VAR NAME=form_control_name . $key>".
Each time the page is reevaluated for output only this variable is evaluted
(to replace it with "selected=selected").

This way the only variable template content is truly dynamic -- could change
from display to display. It seems like this would be a significant
performance boost. (I don't know.)

I put most of the page-specific initialization like this into a module which
is loaded when the filter fires. It gets called from the filter and performs
the work. Because it's a module, variables/objects in the filter's namespace
can be transferred into the module (by reference) using
"$Static_page_init::{$page_name}::variable_name = $self->{my_db_handler}".
References to logging subroutines can be passed in. (I do this just once
after the module is loaded.)

Another advantage to this modular design is that the language-translation
handles, page initialization modules, etc., can be dropped after their use.
Since it's one-time initialization there' no need to keep them around (using
memory). Ultimately, I end up with a single language-translation handle used
for dynamic (display to display) message translation. A very small lexicon
for the page.

I'm very happy with this solution. It does exactly what I was trying to do.
And, makes selection/option lists more static like I'd always thought they
should be. Below is a more detailed example with samples. It may be hard to
follow since this complicated (and involves Locale::Maketext). I could
create a small working example if people thought it would be useful.

Thanks for everyone putting up with my ramblings! I hope this ends up being
useful to someone else.

============== Detailed Example ============

The following describes a way to use H::T to

1. Perform efficient language translation upon templates
2. Minimize the number of duplicated templates which can result from
supporting multiple languages (reducing the duplicated page structure and
ongoing maintenance costs).
3. Cache templates unique by language -- not merely filename.
4. Cache templates in a manner that the bulk of language translation is
cached and not re-evaluated with each display.
5. Use Locale::Maketext in a manner that lexicons are kept small and used as
needed.
6. Avoid retention in memory of language translation objects/packages if, as
stated in #4, the translation is one-time.
7. Use a language negotiation method other than Locale::Maketext (because
the lexicons are used in a way that breaks Locale::Maketext's negotiation
which is based upon a file structure it expects on disk).

The example system uses CGI::App. But, this is not required.

My CGI::App inherets from "superclassMyApp.pm" (which itself inherets from
CGI::Application) containing the following pieces of code:

1. A subclass for H::T's _cache_key method. This method retrieves the
"$language" value that was passed into H::T's load_tmp call. It uses the
value (something like "en-US") to make the cached template unique by path,
filename and language.

==============================>>> CUT HERE
<<<==================================
#***************************************************************************
****
# _cache_key
#
# subclass of H::T's method so we can cache templates by language.
#***************************************************************************
****
sub _cache_key {

my $self = shift;
my $options = $self->{options};

return $self->SUPER::_cache_key().$options->{language};

}
==============================>>> CUT HERE
<<<==================================

2. A method named "prepare_page_for_language" which is called from every
C::A "run_mode" that displays a page. This method:

2a. Contains the following fragment of code to load the template. Notice the
"language =>" parameter. That's what feeds the above-mentioned overriden
_cache_key method.

==============================>>> CUT HERE
<<<==================================
#---------------------------------------------------------------------------
----
# Load the template with site-wide and page-specific filter.
#---------------------------------------------------------------------------
----
$self->{template} = $self->load_tmpl($page . '.html',
    filter => [
       { sub => $filter,
         format => 'scalar' },
       ],
    cache => 1,
    double_file_cache => 1,
    file_cache_dir => '/tmp',
    language => $self->{session}->{LANGUAGE}, # pass this for the custom
_cache_key method
);
==============================>>> CUT HERE
<<<==================================

2b. The filter referred to by the above "load_tmpl." The filter is defined
immediately prior to the above fragment. By creating the filter inside the
same "prepare_page_for_language" method, the anonymous subroutine will be
within scope of all the variables in the method.

The goal of the filter is to initialize a template as much as possible so
that needless H::T re-evaluation does not occur. So that the cached
template's dynamic content is truly part of the display-by-display state
change. In the case of language translation, reevaluating a template's
static text for each display could be significant.

The filter deals with three distinct preprocessing steps:

  1. Language translation that occurs for all site's pages, where the
content is static (header, footer, navigation bar text, title text, etc.).
Once these items are generated they will not change for as long as the page
is cached and redisplayed.

  2. Language translation that occurs for a specific page being loaded,
where the content is static (captions, form titles, sub-area navigation link
text). Similar to site-wide text, this will not change for as long as the
page is cached and redisplayed. This text only exists on this specific page.
The translation handle won't be used for other pages (unlike the static
site-wide handle).

  3. Page initialization that occurs for the specific page, but does not
necessarily involve language translation. For example, A "ROBOTS" "NOINDEX"
might be set using a page-specific initialization routine since, once this
is set it remains this value (like language translation) for as long as the
page remains cached. This routine might generate select/option list values
so that the only thing that has to be re-evaluated (by H::T) is the
"selected" attribute (without having to redo TMPL_LOOPs). The goal is to
eliminate (while we're going to the trouble) stuff that H::T would have to
superflously re-evaluate for each display.

The targets of these three preprocessing steps are represented by <LANG_SITE
NAME=blah>, <LANG_PAGE NAME=blah> and <INIT_PAGE NAME=blah> tags.

The filter follows:

==============================>>> CUT HERE
<<<==================================
#---------------------------------------------------------------------------
----
# Subroutine used by H::T. Must be defined within this CGI::App method (in
order
# to be within scope of the variables it accesses).
#---------------------------------------------------------------------------
----
my $filter = sub {
  my $text_ref = shift;

  $filter_fired = 1; # so we know a page was loaded after tmpl_load (below)


#---------------------------------------------------------------------------
--
  # Load the sitewide language-translation handle for static content if it
  # hasn't already been loaded. (After all the pages are loaded for a
CGI::App
  # module this handle is deleted.)

#---------------------------------------------------------------------------
--
  if
(!exists($self->{LH}{'_sitewide'}{'_static'}{$self->{session}->{LANGUAGE}}))
{
    $self->new_LH('_sitewide', '_static');
  }


#---------------------------------------------------------------------------
--
  # Perform sitewide translation. For every LANG_SITE tag, send the value to
the
  # sitewide language-translation handle (static content).

#---------------------------------------------------------------------------
--
  $$text_ref =~
  s#<LANG_SITE
+NAME\=([^>]*)>#$self->{LH}{'_sitewide'}{'_static'}{$self->{session}->{LANGU
AGE}}->maketext($1)#eg;



#---------------------------------------------------------------------------
--
  # Load the page-specific language translation handle for static content.
  # Process all "LANG_PAGE" tags. Also load the page-specific module for
  # initializing a template in ways beyond "LANG_PAGE" translation.
  #
  # (We have to test if this is not already loaded because H::T calls the
filter
  # multiple times when it loades a template).

#---------------------------------------------------------------------------
--
  if (!exists($self->{LH}{$page}{'_static'}{$self->{session}->{LANGUAGE}}))
{
    $self->new_LH($page, '_static');

    if ($perform_page_init) {
      my $module = 'Static_page_init::' . $page;
      eval "require $module";
    }
  }


#---------------------------------------------------------------------------
--
  # Perform page-specific translation. For every LANG_PAGE tag, send the
value
  # to the page-specific language-translation handle (static content).

#---------------------------------------------------------------------------
--
  $$text_ref =~
  s#<LANG_PAGE
+NAME\=([^>]*)>#$self->{LH}{$page}{'_static'}{$self->{session}->{LANGUAGE}}-
>maketext($1)#eg;



#---------------------------------------------------------------------------
--
  # Call the page-specific initialization routine.

#---------------------------------------------------------------------------
--
  if ($perform_page_init) {
    no strict "refs";
    &{'Static_page_init::' . $page . '::set_static_values'}($text_ref,
$self->{LH}{$page}{'_static'}{$self->{session}->{LANGUAGE}});
    use strict "refs";
  }


#---------------------------------------------------------------------------
--
  # Perform common page initialization.
  # 1. Set the navigation bar's "selected".
  # 2. If the page is not "main", set the "NO" in front of "INDEX". (In the
case
  #    of "main" the tag will be stripped and the page will be indexed.)
  # 3. Eliminate any unset INIT_PAGE tags.

#---------------------------------------------------------------------------
--
  $$text_ref =~
  s#<INIT_PAGE +NAME=$hdr_nav_selected># class="selected"#g;

  if ($page ne 'main') {
    $$text_ref =~
    s#<INIT_PAGE +NAME=HDR_ROBOTS_INDEX>#NO#g;
  }

  $$text_ref =~
  s#<INIT_PAGE +NAME\=[^>]+>##g;

};
==============================>>> CUT HERE
<<<==================================

The filter sets a variable to let me know it was executed by "H::T". It's
the only way I can know after the 'tmpl_load' if H::T performed processing
for a new template, or reused a cached copy.

2c. The following fragment of code is placed after "tmpl_load." If the
filter was executed, this piece of code will

- Get rid of the page-specific language handle (and package) (since neither
are expected to be used again unless H::T senses that it needs to load a
template again to replace a cached copy).

- Adds a pagename to a hash so we can determine when all the pages a C::A
module might display have been loaded (for a language).

- Get rid of the site-wide language handle (and package) if all the pages
have been loaded.

- Loads the page-specific langauge handle for dynamic content (msgs, etc.)
This is used as long as the page remains cached.

==============================>>> CUT HERE
<<<==================================
#---------------------------------------------------------------------------
----
# After loading a page determine if the filter executed. If it did, perform
# post-initialization processing.
#---------------------------------------------------------------------------
----
if ($filter_fired) {

  $filter_fired = 0;

  my $module = $self->{session}->{LANGUAGE};
  $module =~ s/\-/_/;
  $module = lc($module);


#---------------------------------------------------------------------------
--
  # Delete the page-specific language-translation handle for static content,
and
  # the module for page-specific initialization. After a page is loaded and
  # cached these aren't used any longer.

#---------------------------------------------------------------------------
--
  delete($self->{LH}{$page}{'_static'}{$self->{session}->{LANGUAGE}});
  delete_package('lang::' . $page . '::_static::' . $module);
  delete_package('Static_page_init::' . $page);



#---------------------------------------------------------------------------
--
  # Add the page-name to a hash of page-names known to have been loaded.

#---------------------------------------------------------------------------
--
  ${$loaded_pages{$self->{session}->{LANGUAGE}}}{$page} = 1;


#---------------------------------------------------------------------------
--
  # If the total number of pages displayable have been loaded, delete the
site-
  # wide language-translation handle for static content. After all pages are
  # loaded and cached, this isn't needed any longer (unless H::T refreshes
  # the cache, when it will be reloaded in the filter and deleted here,
again.)

#---------------------------------------------------------------------------
--
  if (keys %{$loaded_pages{$self->{session}->{LANGUAGE}}} == $max_pages) {

delete($self->{LH}{'_sitewide'}{'_static'}{$self->{session}->{LANGUAGE}});
    delete_package('lang::_sitewide::_static::' . $module);
  }



#---------------------------------------------------------------------------
--
  # Load the page's language-translation handle for dynamic content. We keep
  # this for each redisplay. (Test if it's already loaded. The cached
template
  # may have been refreshed causing the filter to fire. No need to reload
the
  # handle).

#---------------------------------------------------------------------------
--
  if (!exists($self->{LH}{$page}{'_dynamic'}{$self->{session}->{LANGUAGE}}))
{
    $self->new_LH($page, '_dynamic');
  }

} # end filter fired
==============================>>> CUT HERE
<<<==================================

That's all there is to it.

For additional clarification

1. The superclassMyApp.pm (which my C::A modules inheret from) contains:

  - The following relevant statements:

    use base 'CGI::Application';
    use strict;
    use Symbol qw(delete_package);

  - The following "our" variables:

    our ($max_pages, $perform_page_init, $hdr_nav_selected);
    our (%loaded_pages);

    Those variables are filled in by subclassing C::A's "cgiapp_init" and
calling it with

    cgiapp_init(2, 0, 'HDR_NAV_MAIN');

  - The prepare_page_for_language method, which is called from any C::A
runmode as:

    $self->prepare_page_for_language('main');

    where 'main' is the page to load.

2. The filter checks if the language handles have been created before
creating them. It may not be intuitive, but H::T executes the filter
multiple times when it loads the template. Therefore, it can't be assumed
that the filter is being exeucted for the first or last time (for a page).
The conditional is used to determine if it's necessary to load the language
handles. And, the "filter_fired" variable is used to determine afterwards if
anything happened (so cleanup can occur).

3. The subroutine to create language handles (referenced throughout the
above sample code) looks like the following. It is contained within
superclassMyApp.pm. It loads Locale::Maketext language handles in a more
granular manner than L::M expects. For this reason, it loads the package and
performs a "new" instead of "get_handle" (which performs language
negotiation. I use use "I18N::AcceptLanguage" for that.)

==============================>>> CUT HERE
<<<==================================
#***************************************************************************
****
# new_LH
#
# Common process to create a Locale::Maketext handle. We create 'sitewide',
# 'page::_static' and 'page::_dynamic'. We use Maketext's "->new" method
because
# it has an inefficient language negotiation feature.
#***************************************************************************
****
sub new_LH {

my $self = shift;

my ($page, $type) = @_;

# Start: use maketext's -> new method (bypass negotiation)
my $module = $self->{session}->{LANGUAGE};
$module =~ s/\-/_/;
$module = 'lang::' . $page . '::' . $type . '::' . lc($module);

eval "require $module";

$self->{LH}{$page}{$type}{$self->{session}->{LANGUAGE}} = $module->new();

# End: use maketext's -> new method (bypass negotiation)

return;

}
==============================>>> CUT HERE
<<<==================================

  - The language modules (lexicons) are in a directory structure as follows:

  ~/lang/_sitewide/_static/en_us
  ~/lang/main/_static/en_us
  ~/lang/main/_dynamic/en_us
  ~/lang/main/help/_static/en_us  # examples of lexicons for sub-pages, in
  ~/lang/main/help/_dynamic/en_us  # which case $page is "main::help"

Common translation materials can be shared by using something like this in a
language module (lexicon)

$shared_sidenav = do
'/home/fm/bin/lang/profile/static/shared/sidenav/en_us.include';

Which refers to a file containing:

==============================>>> CUT HERE
<<<==================================
{
sidenav_text =>
(['Area 1',
  'Area 2',
  'Area 3',
  'Area 4',
  'Area 5',
  'Area 6',
  'Area 7']),
sidenav_link_title =>
(['Go to area 1.',
  'Go to area 2.',
  'Go to area 3.',
  'Go to area 4.',
  'Go to area 5.',
  'Go to area 6.',
  'Go to area 7.'])
}
==============================>>> CUT HERE
<<<==================================

The lexicon will then relate these two tags:

<LANG_PAGE NAME=SIDENAV_TEXT> <LANG_PAGE NAME=SIDENAV_TEXT>

to text this way:

'SIDENAV_TEXT' => ${$shared_sidenav}{sidenav_text},
'SIDENAV_LINK_TITLE' => ${$shared_sidenav}{sidenav_link_title},

And return an array ref of the same 7 items through different lexicons
(specific to different pages where the sidenav text is needed).

4. The page-specific initialization modules are stored in the file location
such as:

~/Static_page_init/main.pm
~/Static_page_init/main/help.pm # an example of a sub-page

Common processing (like generating the side-navigation links) can be shared
by required libraries.

=== end



-------------------------------------------------------
This SF.Net email is sponsored by: IntelliVIEW -- Interactive Reporting
Tool for open source databases. Create drag-&-drop reports. Save time
by over 75%! Publish reports on the web. Export to DOC, XLS, RTF, etc.
Download a FREE copy at http://www.intelliview.com/go/osdn_nl
_______________________________________________
Html-template-users mailing list
Html-template-users@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/html-template-users

Reply via email to