I'm a long time user of HTML::Template, but this is my first message to
the list.

I've been trying to handle Language Translations in my web applications,
and have come up with a method that works very cleanly with
HTML::Template.  All it requires is a small patch which I have included
below.

All the patch does is allow you to pass a variable or list of variables
to a filter.  This in turn allows you to do thing like the following:


my $filter = sub {
  my $text_ref = shift;
  my $language = shift;
  my $tr = new My::Translator(language => $language)
  # Send anything in between <LANG> tags for translation
  $$text_ref =~
#<LANG>([^<]*(?:(?!</LANG>)<[^<]*)*)</LANG>#$tr->get($1)#oesxgi;
};
my $template = HTML::Template->new(filename => 'sample.tmpl',
                                   filter => {
                                              sub    => $filter,
                                              format => 'scalar',
                                              args   => ['fr']
                                             }
);

Notice the 'args' entry in the filter section.  When this template is
loaded, the filter will parse out all text that is between <LANG> tags
and send them to the Translator module for translation.  For the
Translator module you could use something like Locale::Maketext or
Locale::PGetText or you could brew your own...  So if the next time a
user wants the page in German, you pass 'de' to the filter instead of
'fr'.

Where this system really comes in handy, is when you use
HTML::Template's caching mechanisms.  All filters are run before caching
takes place, so the next time you load a template, it pulls it straight
out of the cache without the overhead of running the filters, or having
to reparse the template.  So a language translation is only expensive
the first time it is called.

The drawback to this is that the cache will fill up quicker the more
languages you support.  Eventually the cache will contain N x M
templates where N is the number of templates, and M is the number of
languages you support.  However, using the file_cache this shouldn't be
an problem (disk space is cheap).

So to sum up...

Advantages:
- no need to duplicate your templates to support multiple languages.
- translation is done once and then cached (if you have caching turned
on)
- should work with any module that supports a 'gettext' type interface
- the patch doesn't alter HTML::Template functionality, it just adds
functionality to the filter mechanism.


Drawbacks:
- cache will be bigger (not a big deal if you use file_cache)
- only does one to one translations (ie you can't dynamically translate
dates or currency)


Comments on the patch:

Adding support for filter args was quite simple.  It only required a
couple of extra lines of code.  However, a big part of the patch applies
to the caching mechanisms of HTML::Template.  Cache lookups now need to
consider arguements passed to the filter, so I have had to alter the way
in which caching works.
The way I chose to do this is to generate a 'cachekey' based on the full
filepath, and a combination of the filter args, and then generating an
MD5 hash of the resulting string.  This has the undesirable effect of
invalidating historical caches if people apply the patch and restart
their applications.  So a cache flush should be done after an upgrade. 
There would probably be a way to use the old mechanism if filter args
were not used, and only use the new cachekey method when they are used,
but that seemed more complex than desirable for my taste.  One other
result of the patch is that the Digest::MD5 is required for all caching
mechanisms (before it was only required for the file_cache).

Also, perhaps this has been brought up before, but the Cache::Cache
module seems to have stabilized quite nicely.  Would it not be
appropriate to remove all caching mechanisms from HTML::Template and use
the Cache::Cache module instead (it handles size aware file and shared
memory caches).  This lets HTML::Template do what it does best  ...parse
templates...  and leaves caching to a module that focuses purely on
caching...  Just my 2 cents...


anyway,  any comments would be appreciated?

--
Cees Hek


below are patches for Template.pm and test.pl


diff -c HTML-Template-2.5_orig/Template.pm HTML-Template-2.5/Template.pm
*** HTML-Template-2.5_orig/Template.pm  Sat Feb  2 10:01:37 2002
--- HTML-Template-2.5/Template.pm       Tue Jul  2 15:21:38 2002
***************
*** 806,813 ****
  
  filter - this option allows you to specify a filter for your template
  files.  A filter is a subroutine that will be called after
! HTML::Template reads your template file but before it starts parsing
! template tags.
  
  In the most simple usage, you simply assign a code reference to the
  filter parameter.  This subroutine will recieve a single arguement - a
--- 806,813 ----
  
  filter - this option allows you to specify a filter for your template
  files.  A filter is a subroutine that will be called after
! HTML::Template reads your template file but before it places it in the
! cache and before it starts parsing template tags.
  
  In the most simple usage, you simply assign a code reference to the
  filter parameter.  This subroutine will recieve a single arguement - a
***************
*** 825,831 ****
                                        filter => $filter);
  
  More complicated usages are possible.  You can request that your
! filter receieve the template text as an array of lines rather than as
  a single scalar.  To do that you need to specify your filter using a
  hash-ref.  In this form you specify the filter using the "sub" key and
  the desired argument format using the "format" key.  The available
--- 825,831 ----
                                        filter => $filter);
  
  More complicated usages are possible.  You can request that your
! filter receive the template text as an array of lines rather than as
  a single scalar.  To do that you need to specify your filter using a
  hash-ref.  In this form you specify the filter using the "sub" key and
  the desired argument format using the "format" key.  The available
***************
*** 836,841 ****
--- 836,868 ----
                                        filter => { sub => $filter,
                                                    format => 'array' });
  
+ You can also pass the filter extra parameters.  To do that you need to
+ specify your filter using a hash-ref.  Allong with the "sub" key you
+ can also provide an "args" key with a list of arguements to pass to the
+ filter.  When the filter is called, the list of args will be passed
+ first, and then the template in the "format" that you specified.
+ Since filters are applied before caching takes place, the "args" will
+ be considered in cache lookups.  So if you access the same template
+ with different "args" to the filter, you will end up with two copies of
+ the template in the cache.  Currently you can only pass scalar values
+ to the filter.  The example below provides a simple technique for
+ localizing your templates.
+ 
+    my $filter = sub {
+      my $text_ref = shift;
+      my $language = shift;
+      my $translator = new My::Translator(language => $language)
+      # Send anything in between <LANG> tags for translation
+      $$text_ref =~ 
+s#<LANG>([^<]*(?:(?!</LANG>)<[^<]*)*)</LANG>#$translator->get($1)#oesxgi;
+    };
+    my $template = HTML::Template->new(filename => 'zap.tmpl',
+                                       filter => {
+                                                  sub    => $filter,
+                                                  format => 'scalar',
+                                                  args   => ['fr']
+                                                 }
+    );
+ 
  You may also have multiple filters.  This allows simple filters to be
  combined for more elaborate functionality.  To do this you specify an
  array of filters.  The filters are applied in the order they are
***************
*** 983,988 ****
--- 1010,1020 ----
    if (ref($options->{filter}) ne 'ARRAY') {
      $options->{filter} = [ $options->{filter} ];
    }
+ 
+   # filter args should be an array if it's not already
+   foreach my $filter (@{$options->{filter}}) {
+     $filter->{args} = [ $filter->{args} ] if ( ref $filter eq 'HASH' && 
+$filter->{args} && ref $filter->{args} ne 'ARRAY');
+   }
    
    # make sure objects in associate area support param()
    foreach my $object (@{$options->{associate}}) {
***************
*** 1011,1016 ****
--- 1043,1055 ----
      print STDERR "\n### HTML::Template Memory Debug ### START ", 
$self->{proc_mem}->size(), "\n";
    }
  
+   if ($options->{cache}) {
+     # The caching system needs Digest::MD5
+     eval { require Digest::MD5; };
+     croak("Could not load Digest::MD5.  You must have Digest::MD5 installed to use 
+HTML::Template in file_cache mode.  The error was: $@")
+       if ($@);
+   }
+ 
    if ($options->{file_cache}) {
      # make sure we have a file_cache_dir option
      croak("You must specify the file_cache_dir option if you want to use 
file_cache.") 
***************
*** 1021,1029 ****
      eval { require Storable; };
      croak("Could not load Storable.  You must have Storable installed to use 
HTML::Template in file_cache mode.  The error was: $@")
        if ($@);
-     eval { require Digest::MD5; };
-     croak("Could not load Digest::MD5.  You must have Digest::MD5 installed to use 
HTML::Template in file_cache mode.  The error was: $@")
-       if ($@);
    }
  
    if ($options->{shared_cache}) {
--- 1060,1065 ----
***************
*** 1173,1178 ****
--- 1209,1245 ----
  # Caching subroutines - they handle getting and validating cache
  # records from either the in-memory or shared caches.
  
+ # generates MD5 from filepath and the args for any filters to 
+ # determine a unique key to use for caching
+ sub _get_cache_key {
+   my $self = shift;
+   my $options = $self->{options};
+ 
+   return unless exists($options->{filename});
+   my $key = $options->{filepath} || $self->_find_file($options->{filename});
+ 
+   # Create a unique string out of all the filter "args"
+   # by joining the "args" list with a counter specifying the
+   # position of the filter.  The counter is used to stop filter 
+   # "args" like the following from being equal:  
+   #     filter 1 - args => ['a', 'b', 'c']
+   # - and -
+   #     filter 1 - args => ['a', 'b'], filter 2 - args => ['c']
+   my $i = 1;
+   foreach my $filter (@{$options->{filter}}) {
+     if (ref $filter eq 'HASH' and $filter->{args}) {
+       $key .= $i.join($i, @{$filter->{args}});
+     }
+     $i++;
+   }
+ 
+   # hash it ...
+   my $hash = Digest::MD5::md5_hex($key);
+   $options->{cache_debug} and print STDERR "### HTML::Template Cache Debug ### 
+CACHEKEY : $hash  raw($key)\n";
+ 
+   return $hash;
+ }
+ 
  # handles the normal in memory cache
  use vars qw( %CACHE );
  sub _fetch_from_cache {
***************
*** 1182,1198 ****
    # return if there's no cache entry for this filename
    return unless exists($options->{filename});
    my $filepath = $self->_find_file($options->{filename});
!   return unless (defined($filepath) and
!                  exists $CACHE{$filepath});
    
    $options->{filepath} = $filepath;
  
    # validate the cache
    my $mtime = $self->_mtime($filepath);  
    if (defined $mtime) {
      # return if the mtime doesn't match the cache
!     if (defined($CACHE{$filepath}{mtime}) and 
!         ($mtime != $CACHE{$filepath}{mtime})) {
        $options->{cache_debug} and 
          print STDERR "CACHE MISS : $filepath : $mtime\n";
        return;
--- 1249,1268 ----
    # return if there's no cache entry for this filename
    return unless exists($options->{filename});
    my $filepath = $self->_find_file($options->{filename});
!   my $cachekey = $self->_get_cache_key();
! 
!   return unless (defined($cachekey) and
!                  exists $CACHE{$cachekey});
    
    $options->{filepath} = $filepath;
+   $options->{cachekey} = $cachekey;
  
    # validate the cache
    my $mtime = $self->_mtime($filepath);  
    if (defined $mtime) {
      # return if the mtime doesn't match the cache
!     if (defined($CACHE{$cachekey}{mtime}) and 
!         ($mtime != $CACHE{$cachekey}{mtime})) {
        $options->{cache_debug} and 
          print STDERR "CACHE MISS : $filepath : $mtime\n";
        return;
***************
*** 1200,1212 ****
  
      # if the template has includes, check each included file's mtime
      # and return if different
!     if (exists($CACHE{$filepath}{included_mtimes})) {
!       foreach my $filename (keys %{$CACHE{$filepath}{included_mtimes}}) {
          next unless 
!           defined($CACHE{$filepath}{included_mtimes}{$filename});
          
          my $included_mtime = (stat($filename))[9];
!         if ($included_mtime != $CACHE{$filepath}{included_mtimes}{$filename}) {
            $options->{cache_debug} and 
              print STDERR "### HTML::Template Cache Debug ### CACHE MISS : $filepath 
: INCLUDE $filename : $included_mtime\n";
            
--- 1270,1282 ----
  
      # if the template has includes, check each included file's mtime
      # and return if different
!     if (exists($CACHE{$cachekey}{included_mtimes})) {
!       foreach my $filename (keys %{$CACHE{$cachekey}{included_mtimes}}) {
          next unless 
!           defined($CACHE{$cachekey}{included_mtimes}{$filename});
          
          my $included_mtime = (stat($filename))[9];
!         if ($included_mtime != $CACHE{$cachekey}{included_mtimes}{$filename}) {
            $options->{cache_debug} and 
              print STDERR "### HTML::Template Cache Debug ### CACHE MISS : $filepath 
: INCLUDE $filename : $included_mtime\n";
            
***************
*** 1220,1229 ****
    
    $options->{cache_debug} and print STDERR "### HTML::Template Cache Debug ### CACHE 
HIT : $filepath\n";
        
!   $self->{param_map} = $CACHE{$filepath}{param_map};
!   $self->{parse_stack} = $CACHE{$filepath}{parse_stack};
!   exists($CACHE{$filepath}{included_mtimes}) and
!     $self->{included_mtimes} = $CACHE{$filepath}{included_mtimes};
  
    # clear out values from param_map from last run
    $self->_normalize_options();
--- 1290,1299 ----
    
    $options->{cache_debug} and print STDERR "### HTML::Template Cache Debug ### CACHE 
HIT : $filepath\n";
        
!   $self->{param_map} = $CACHE{$cachekey}{param_map};
!   $self->{parse_stack} = $CACHE{$cachekey}{parse_stack};
!   exists($CACHE{$cachekey}{included_mtimes}) and
!     $self->{included_mtimes} = $CACHE{$cachekey}{included_mtimes};
  
    # clear out values from param_map from last run
    $self->_normalize_options();
***************
*** 1234,1239 ****
--- 1304,1310 ----
    my $self = shift;
    my $options = $self->{options};
  
+   my $cachekey = $options->{cachekey} || $self->_get_cache_key();
    my $filepath = $options->{filepath};
    if (not defined $filepath) {
      $filepath = $self->_find_file($options->{filename});
***************
*** 1242,1255 ****
      $options->{filepath} = $filepath;   
    }
  
!   $options->{cache_debug} and print STDERR "### HTML::Template Cache Debug ### CACHE 
LOAD : $filepath\n";
      
    $options->{blind_cache} or
!     $CACHE{$filepath}{mtime} = $self->_mtime($filepath);
!   $CACHE{$filepath}{param_map} = $self->{param_map};
!   $CACHE{$filepath}{parse_stack} = $self->{parse_stack};
    exists($self->{included_mtimes}) and
!     $CACHE{$filepath}{included_mtimes} = $self->{included_mtimes};
  }
  
  # generates MD5 from filepath to determine filename for cache file
--- 1313,1326 ----
      $options->{filepath} = $filepath;   
    }
  
!   $options->{cache_debug} and print STDERR "### HTML::Template Cache Debug ### CACHE 
LOAD : $filepath - $cachekey\n";
      
    $options->{blind_cache} or
!     $CACHE{$cachekey}{mtime} = $self->_mtime($filepath);
!   $CACHE{$cachekey}{param_map} = $self->{param_map};
!   $CACHE{$cachekey}{parse_stack} = $self->{parse_stack};
    exists($self->{included_mtimes}) and
!     $CACHE{$cachekey}{included_mtimes} = $self->{included_mtimes};
  }
  
  # generates MD5 from filepath to determine filename for cache file
***************
*** 1257,1263 ****
    my ($self, $filepath) = @_;
  
    # hash the filename ...
!   my $hash = Digest::MD5::md5_hex($filepath);
    
    # ... and build a path out of it.  Using the first two charcters
    # gives us 255 buckets.  This means you can have 255,000 templates
--- 1328,1335 ----
    my ($self, $filepath) = @_;
  
    # hash the filename ...
! #  my $hash = Digest::MD5::md5_hex($filepath);
!   my $hash = $self->_get_cache_key();
    
    # ... and build a path out of it.  Using the first two charcters
    # gives us 255 buckets.  This means you can have 255,000 templates
***************
*** 1292,1298 ****
    croak("HTML::Template::new() - Problem reading cache file $cache_filename 
(file_cache => 1) : $!") 
      unless defined $self->{record};
  
!   ($self->{mtime}, 
     $self->{included_mtimes}, 
     $self->{param_map}, 
     $self->{parse_stack}) = @{$self->{record}};
--- 1364,1371 ----
    croak("HTML::Template::new() - Problem reading cache file $cache_filename 
(file_cache => 1) : $!") 
      unless defined $self->{record};
  
!   ($self->{filepath}, 
!    $self->{mtime}, 
     $self->{included_mtimes}, 
     $self->{param_map}, 
     $self->{parse_stack}) = @{$self->{record}};
***************
*** 1325,1334 ****
          if ($included_mtime != $self->{included_mtimes}{$filename}) {
            $options->{cache_debug} and 
              print STDERR "### HTML::Template Cache Debug ### FILE CACHE MISS : 
$filepath : INCLUDE $filename : $included_mtime\n";
!           ($self->{mtime}, 
             $self->{included_mtimes}, 
             $self->{param_map}, 
!            $self->{parse_stack}) = (undef, undef, undef, undef);
            return;
          }
        }
--- 1398,1408 ----
          if ($included_mtime != $self->{included_mtimes}{$filename}) {
            $options->{cache_debug} and 
              print STDERR "### HTML::Template Cache Debug ### FILE CACHE MISS : 
$filepath : INCLUDE $filename : $included_mtime\n";
!           ($self->{filepath}, 
!            $self->{mtime}, 
             $self->{included_mtimes}, 
             $self->{param_map}, 
!            $self->{parse_stack}) = (undef, undef, undef, undef, undef);
            return;
          }
        }
***************
*** 1370,1376 ****
  
    my $result;
    eval {
!     $result = Storable::lock_store([ $self->{mtime},
                                       $self->{included_mtimes}, 
                                       $self->{param_map}, 
                                       $self->{parse_stack} ],
--- 1444,1451 ----
  
    my $result;
    eval {
!     $result = Storable::lock_store([ $self->{filepath},
!                                      $self->{mtime},
                                       $self->{included_mtimes}, 
                                       $self->{param_map}, 
                                       $self->{parse_stack} ],
***************
*** 1390,1400 ****
  
    my $filepath = $self->_find_file($options->{filename});
    return unless defined $filepath;
  
    # fetch from the shared cache.
!   $self->{record} = $self->{cache}{$filepath};
!   
!   ($self->{mtime}, 
     $self->{included_mtimes}, 
     $self->{param_map}, 
     $self->{parse_stack}) = @{$self->{record}}
--- 1465,1477 ----
  
    my $filepath = $self->_find_file($options->{filename});
    return unless defined $filepath;
+   my $cachekey = $self->_get_cache_key();
  
    # fetch from the shared cache.
!   $self->{record} = $self->{cache}{$cachekey};
! 
!   ($self->{filepath}, 
!    $self->{mtime}, 
     $self->{included_mtimes}, 
     $self->{param_map}, 
     $self->{parse_stack}) = @{$self->{record}}
***************
*** 1410,1423 ****
  }
  
  sub _validate_shared_cache {
!   my ($self, $filename, $record) = @_;
    my $options = $self->{options};
  
-   $options->{shared_cache_debug} and print STDERR "### HTML::Template Cache Debug 
### SHARED CACHE VALIDATE : $filename\n";
- 
    return 1 if $options->{blind_cache};
  
!   my ($c_mtime, $included_mtimes, $param_map, $parse_stack) = @$record;
  
    # if the modification time has changed return false
    my $mtime = $self->_mtime($filename);
--- 1487,1502 ----
  }
  
  sub _validate_shared_cache {
!   my ($self, $cachekey, $record) = @_;
    my $options = $self->{options};
  
    return 1 if $options->{blind_cache};
  
!   my ($filename, $c_mtime, $included_mtimes, $param_map, $parse_stack) = @$record;
! 
!   return unless $filename;
! 
!   $options->{shared_cache_debug} and print STDERR "### HTML::Template Cache Debug 
### SHARED CACHE VALIDATE : $filename\n";
  
    # if the modification time has changed return false
    my $mtime = $self->_mtime($filename);
***************
*** 1458,1464 ****
    print STDERR "### HTML::Template Memory Debug ### END CACHE LOAD ", 
$self->{proc_mem}->size(), "\n"
      if $options->{memory_debug};
  
!   return [ $self->{mtime},
             $self->{included_mtimes}, 
             $self->{param_map}, 
             $self->{parse_stack} ]; 
--- 1537,1544 ----
    print STDERR "### HTML::Template Memory Debug ### END CACHE LOAD ", 
$self->{proc_mem}->size(), "\n"
      if $options->{memory_debug};
  
!   return [ $self->{filepath},
!            $self->{mtime},
             $self->{included_mtimes}, 
             $self->{param_map}, 
             $self->{parse_stack} ]; 
***************
*** 1600,1606 ****
    my $template_ref = shift;
    my $options = $self->{options};
  
!   my ($format, $sub);
    foreach my $filter (@{$options->{filter}}) {
      croak("HTML::Template->new() : bad value set for filter parameter - must be a 
code ref or a hash ref.")
        unless ref $filter;
--- 1680,1686 ----
    my $template_ref = shift;
    my $options = $self->{options};
  
!   my ($format, $sub, $args);
    foreach my $filter (@{$options->{filter}}) {
      croak("HTML::Template->new() : bad value set for filter parameter - must be a 
code ref or a hash ref.")
        unless ref $filter;
***************
*** 1612,1617 ****
--- 1692,1698 ----
      if (ref $filter eq 'HASH') {
        $format = $filter->{'format'};
        $sub = $filter->{'sub'};
+       $args = $filter->{'args'} || [];
  
        # check types and values
        croak("HTML::Template->new() : bad value set for filter parameter - hash must 
contain \"format\" key and \"sub\" key.")
***************
*** 1620,1636 ****
          unless $format eq 'array' or $format eq 'scalar';
        croak("HTML::Template->new() : bad value set for filter parameter - \"sub\" 
must be a code ref")
          unless ref $sub and ref $sub eq 'CODE';
  
        # catch errors
        eval {
          if ($format eq 'scalar') {
            # call
!           $sub->($template_ref);
          } else {
          # modulate
          my @array = map { $_."\n" } split("\n", $$template_ref);
            # call
!           $sub->(\@array);
          # demodulate
          $$template_ref = join("", @array);
          }
--- 1701,1721 ----
          unless $format eq 'array' or $format eq 'scalar';
        croak("HTML::Template->new() : bad value set for filter parameter - \"sub\" 
must be a code ref")
          unless ref $sub and ref $sub eq 'CODE';
+       foreach my $arg (@$args) {
+         croak("HTML::Template->new() : bad value set for filter parameter - \"args\" 
+can only contain scalars")
+           if ref $arg;
+       }
  
        # catch errors
        eval {
          if ($format eq 'scalar') {
            # call
!           $sub->($template_ref, @$args);
          } else {
          # modulate
          my @array = map { $_."\n" } split("\n", $$template_ref);
            # call
!           $sub->(\@array, @$args);
          # demodulate
          $$template_ref = join("", @array);
          }
diff -c HTML-Template-2.5_orig/test.pl HTML-Template-2.5/test.pl
*** HTML-Template-2.5_orig/test.pl      Tue Jan 29 15:03:46 2002
--- HTML-Template-2.5/test.pl   Tue Jul  2 14:00:01 2002
***************
*** 613,618 ****
--- 613,666 ----
  $output =  $template->output;
  ok($output =~ /Zanzabar!!!/);
  
+ $template = HTML::Template->new(filename => './templates/include_path/a.tmpl',
+                                 cache => 1,
+                                 filter => {
+                                            sub => sub {
+                                                ${$_[0]} =~ s/Bar/$_[1]/g;
+                                            },
+                                            format => 'scalar',
+                                            args => 'Zanzabar',
+                                          },
+                                );
+ $output =  $template->output;
+ ok($output =~ /Zanzabar/);
+ 
+ $template = HTML::Template->new(filename => './templates/include_path/a.tmpl',
+                                 cache => 1,
+                                 filter => {
+                                            sub => sub {
+                                                ${$_[0]} =~ s/$_[1]/$_[2]/g;
+                                            },
+                                            format => 'scalar',
+                                            args => ['Bar','Casbah'],
+                                          },
+                                );
+ $output =  $template->output;
+ ok($output =~ /Casbah/);
+ 
+ $template = HTML::Template->new(filename => './templates/include_path/a.tmpl',
+                                 cache => 1,
+                                 filter => [
+                                            {
+                                             sub => sub {
+                                                ${$_[0]} =~ s/Bar/$_[1]/g;
+                                             },
+                                             format => 'scalar',
+                                             args => ['Casbah'],
+                                            },
+                                            {
+                                             sub => sub {
+                                                ${$_[0]} =~ s/bah/$_[1]/g;
+                                             },
+                                             format => 'scalar',
+                                             args => ['bar'],
+                                            },
+                                           ]
+                                );
+ $output =  $template->output;
+ ok($output =~ /Casbar/);
+ 
  my $x;
  $template = HTML::Template->new(filename => './templates/include_path/a.tmpl',
                                  filter => {


---------------------------------------------------------------------
To unsubscribe, e-mail: [EMAIL PROTECTED]
For additional commands, e-mail: [EMAIL PROTECTED]

Reply via email to