use HTML::Parser ();

...;

sub translate_template {
    my $self = shift;
    my ( $lang, $template ) = @_;

    my ( $langlong, $langshort ) = ( $lang =~ m/(([^-]+)(?:-.+)?)/ );

    my $replaces = {
        'langshort' => $langshort,
        'langlong'  => $langlong,
        'langdir'   => 'ltr',
        'countries' => $self->countries_menu($lang),
    };
    if ( $self->{'locales'}{$lang}->get_character_orientation_from_code() eq 'right-to-left' ) {
        $replaces->{'langdir'} = 'rtl';
    }

    foreach my $replace ( keys %$replaces ) {
        $template =~ s/!!\$$replace/$replaces->{$replace}/g;
    }

    my $p = HTML::Parser->new(
        api_version   => 3,
        default_h     => [ \&translate_template_parser, 'event,tagname,attr,attrseq,text,dtext,self' ],
        unbroken_text => 1,
        xml_pic       => 1,
    );
    $p->{'catalog'} = $self->{'catalog'};
    $p->{'lang'}    = $lang;
    $p->{'return'}  = '';
    $p->parse($template);
    $p->eof;
    $template = $p->{'return'};

    return $template;
}

sub translate_template_parser {
    state $closed_tags = set( 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr' );
    state $depth       = 0;
    state $intransl    = 0;
    state $buf         = '';
    my ( $event, $tag, $attr, $attrseq, $text, $dtext, $obj ) = @_;

    # pre-closed tags shouldn't increase or decrease our depth
    if ( $closed_tags->member($tag) ) { $event = 'closed'; }
    if ( defined($attrseq) and scalar(@$attrseq) and ( $attrseq->[ -1 ] eq '/' ) ) { $event = 'closed'; }

    my $append_text = $dtext // $text;

    if ( $event eq 'start' ) {
        ## go down a level
        $buf .= $append_text;
        if ( $attr->{'data-gettext'} ) {
            $obj->{'return'} .= $buf;
            $buf      = '';
            $intransl = $depth;
        }
        $depth += 1;
    }
    elsif ( $event eq 'end' ) {
        ## come up a level, appending translated text
        $depth -= 1;
        if ( $intransl == $depth ) {
            for ($buf) { s/\A\s+//; s/\s+\z//; }
            $obj->{'return'} .= $obj->{'catalog'}->lookup( $obj->{'lang'}, $buf )->msgstr;
            $buf      = '';
            $intransl = 0;
        }
        $buf .= $append_text;
    }
    elsif ( $event eq 'end_document' ) {
        $obj->{'return'} .= $buf;
    }
    else {
        $buf .= $append_text;
    }
}

...;