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