summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2016-07-06 11:53:13 -0700
committerMark Wells <mark@freeside.biz>2016-07-06 11:53:13 -0700
commitf413badbfe4676563d11b528838a21d9ceb8da14 (patch)
treecfd1749f7241890cb08b369aca1959cb18e1003f
parentdf43a12a1c219709f27297ad6b53f6662b56c10c (diff)
service label localization, internals and UI, #71347
-rw-r--r--FS/FS/Mason.pm1
-rw-r--r--FS/FS/Schema.pm18
-rw-r--r--FS/FS/part_svc.pm27
-rw-r--r--FS/FS/part_svc_msgcat.pm131
-rwxr-xr-xhttemplate/browse/part_svc.cgi20
-rw-r--r--httemplate/edit/elements/part_svc_column.html5
-rwxr-xr-xhttemplate/edit/part_pkg.cgi75
-rwxr-xr-xhttemplate/edit/part_svc.cgi52
-rw-r--r--httemplate/edit/process/elements/process.html21
-rwxr-xr-xhttemplate/edit/process/part_pkg.cgi5
-rw-r--r--httemplate/elements/freeside.css2
-rw-r--r--httemplate/elements/progress-init.html14
-rw-r--r--httemplate/elements/tr-input-locale-text.html120
13 files changed, 385 insertions, 106 deletions
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 24ddf79..d625fbd 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -412,6 +412,7 @@ if ( -e $addl_handler_use_file ) {
use FS::fiber_olt;
use FS::olt_site;
use FS::access_user_page_pref;
+ use FS::part_svc_msgcat;
# Sammath Naur
if ( $FS::Mason::addl_handler_use ) {
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index a50b551..f88fea9 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -3682,6 +3682,24 @@ sub tables_hashref {
],
},
+ 'part_svc_msgcat' => {
+ 'columns' => [
+ 'svcpartmsgnum', 'serial', '', '', '', '',
+ 'svcpart', 'int', '', '', '', '',
+ 'locale', 'varchar', '', 16, '', '',
+ 'svc', 'varchar', '', $char_d, '', '',
+ ],
+ 'primary_key' => 'svcpartmsgnum',
+ 'unique' => [ [ 'svcpart', 'locale' ] ],
+ 'index' => [],
+ 'foreign_keys' => [
+ { columns => [ 'svcpart' ],
+ table => 'part_svc',
+ },
+ ],
+ },
+
+
#(this should be renamed to part_pop)
'svc_acct_pop' => {
'columns' => [
diff --git a/FS/FS/part_svc.pm b/FS/FS/part_svc.pm
index 621a554..dcc7843 100644
--- a/FS/FS/part_svc.pm
+++ b/FS/FS/part_svc.pm
@@ -1,5 +1,5 @@
package FS::part_svc;
-use base qw(FS::Record);
+use base qw(FS::o2m_Common FS::Record);
use strict;
use vars qw( $DEBUG );
@@ -11,6 +11,7 @@ use FS::part_export;
use FS::export_svc;
use FS::cust_svc;
use FS::part_svc_class;
+use FS::part_svc_msgcat;
FS::UID->install_callback(sub {
# preload the cache and make sure all modules load
@@ -621,6 +622,24 @@ sub svc_x {
map { $_->svc_x } $self->cust_svc;
}
+=item svc_locale LOCALE
+
+Returns a customer-viewable service definition label in the chosen LOCALE.
+If there is no entry for that locale or if LOCALE is empty, returns
+part_svc.svc.
+
+=cut
+
+sub svc_locale {
+ my( $self, $locale ) = @_;
+ return $self->svc unless $locale;
+ my $part_svc_msgcat = qsearchs('part_svc_msgcat', {
+ svcpart => $self->svcpart,
+ locale => $locale
+ }) or return $self->svc;
+ $part_svc_msgcat->svc;
+}
+
=back
=head1 CLASS METHODS
@@ -883,6 +902,12 @@ sub process {
$param->{'svcpart'} = $new->getfield('svcpart');
}
+ $error ||= $new->process_o2m(
+ 'table' => 'part_svc_msgcat',
+ 'params' => $param,
+ 'fields' => [ 'locale', 'svc' ],
+ );
+
die "$error\n" if $error;
}
diff --git a/FS/FS/part_svc_msgcat.pm b/FS/FS/part_svc_msgcat.pm
new file mode 100644
index 0000000..6d69198
--- /dev/null
+++ b/FS/FS/part_svc_msgcat.pm
@@ -0,0 +1,131 @@
+package FS::part_svc_msgcat;
+use base qw( FS::Record );
+
+use strict;
+use FS::Locales;
+
+=head1 NAME
+
+FS::part_svc_msgcat - Object methods for part_svc_msgcat records
+
+=head1 SYNOPSIS
+
+ use FS::part_svc_msgcat;
+
+ $record = new FS::part_svc_msgcat \%hash;
+ $record = new FS::part_svc_msgcat { 'column' => 'value' };
+
+ $error = $record->insert;
+
+ $error = $new_record->replace($old_record);
+
+ $error = $record->delete;
+
+ $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::part_svc_msgcat object represents localized labels of a service
+definition. FS::part_svc_msgcat inherits from FS::Record. The following
+fields are currently supported:
+
+=over 4
+
+=item svcpartmsgnum
+
+primary key
+
+=item svcpart
+
+Service definition
+
+=item locale
+
+locale
+
+=item svc
+
+Localized service name (customer-viewable)
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new record. To add the record to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to. You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'part_svc_msgcat'; }
+
+=item insert
+
+Adds this record to the database. If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database. If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid record. If there is
+an error, returns the error, otherwise returns false. Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+ my $self = shift;
+
+ my $error =
+ $self->ut_numbern('svcpartmsgnum')
+ || $self->ut_foreign_key('svcpart', 'part_svc', 'svcpart')
+ || $self->ut_enum('locale', [ FS::Locales->locales ] )
+ || $self->ut_text('svc')
+ ;
+ return $error if $error;
+
+ $self->SUPER::check;
+}
+
+=back
+
+=head1 BUGS
+
+=head1 SEE ALSO
+
+L<FS::part_svc>, L<FS::Record>, schema.html from the base documentation.
+
+=cut
+
+1;
+
diff --git a/httemplate/browse/part_svc.cgi b/httemplate/browse/part_svc.cgi
index dee4394..b947463 100755
--- a/httemplate/browse/part_svc.cgi
+++ b/httemplate/browse/part_svc.cgi
@@ -112,8 +112,24 @@ function part_export_areyousure(href) {
</TD>
% }
- <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>"><A HREF="<% $url %>">
- <% $part_svc->svc %></A></TD>
+ <TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
+ <A HREF="<% $url %>">
+ <% $part_svc->svc %>
+ </A>
+% # any alternate names of the service
+% my %msgcat = map { $_->locale => $_ } $part_svc->part_svc_msgcat;
+% my %labels = map { $_ => FS::Locales->description($_) } keys %msgcat;
+% my @locales = sort { $labels{$a} cmp $labels{$b} } keys %msgcat;
+% if ( @locales ) {
+ <BR>
+ <FONT SIZE="-1">
+% foreach my $locale (@locales) {
+ <% $labels{$locale} %>: <% $msgcat{$locale}->get('svc') %>
+ <BR>
+% }
+ </FONT>
+% }
+ </TD>
<TD ROWSPAN=<% $rowspan %> CLASS="grid" BGCOLOR="<% $bgcolor %>">
<% $svcdb %></TD>
diff --git a/httemplate/edit/elements/part_svc_column.html b/httemplate/edit/elements/part_svc_column.html
index 4e112c0..816f342 100644
--- a/httemplate/edit/elements/part_svc_column.html
+++ b/httemplate/edit/elements/part_svc_column.html
@@ -267,8 +267,9 @@ my %communigate_fields = (
<& /elements/progress-init.html,
$svcdb, #form name
[ # form fields to send
- qw(svc svcpart classnum selfservice_access disabled preserve exportnum),
- @fields
+ 'ALL'
+# qw(svc svcpart classnum selfservice_access disabled preserve exportnum),
+# @fields
],
'process/part_svc.cgi', # target
$p.'browse/part_svc.cgi', # redirect landing
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
index 80a61f8..f2c4aac 100755
--- a/httemplate/edit/part_pkg.cgi
+++ b/httemplate/edit/part_pkg.cgi
@@ -92,7 +92,7 @@
{ type => 'columnstart' },
{ field => 'pkg',
- type => 'text',
+ type => 'input-locale-text',
size => 40, #32
maxlength => 50,
},
@@ -495,42 +495,6 @@ my $recur_show_zero_disabled = 1;
my $pkgpart = '';
-my $splice_locale_fields = sub {
- my( $fields, $pkey_value_callback, $pkg_value_callback ) = @_;
-
- my $n = 0;
- my @locale_fields = (
- map {
- my $pkey_value= $pkey_value_callback ? &$pkey_value_callback($_) : '';
- my $pkg_value = $pkg_value_callback
- ? $pkg_value_callback eq 'cgiparam'
- ? $cgi->param('pkgpartmsgnum'. $n. '_pkg')
- : &$pkg_value_callback($_)
- : '';
- (
- { field => 'pkgpartmsgnum'. $n,
- type => 'hidden',
- value => $pkey_value,
- },
- { field => 'pkgpartmsgnum'. $n. '_locale',
- type => 'hidden',
- value => $_,
- },
- { field => 'pkgpartmsgnum'. $n++. '_pkg',
- type => 'text',
- size => 40,
- #maxlength => 50,
- value => $pkg_value,
- },
- );
-
- }
- @locales
- );
- splice(@$fields, 7, 0, @locale_fields); #XXX 7 is arbitrary above
-
-};
-
my $error_callback = sub {
my($cgi, $object, $fields, $opt ) = @_;
@@ -579,16 +543,6 @@ my $error_callback = sub {
$pkgpart = $object->pkgpart;
- &$splice_locale_fields(
- $fields,
- sub {
- my $locale = shift;
- my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
- $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
- },
- 'cgiparam'
- );
-
if ( $cgi->param('error') =~ / is suggested with / ) {
#yeah, detection is a shitty kludge, but we don't have exception objects
$opt->{form_init} = '<INPUT TYPE="checkbox" NAME="part_pkg_restrict_soft_override" VALUE="Y"> Override suggestion<BR><BR>';
@@ -665,20 +619,6 @@ my $edit_callback = sub {
$pkgpart = $object->pkgpart;
- &$splice_locale_fields(
- $fields,
- sub {
- my $locale = shift;
- my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
- $part_pkg_msgcat ? $part_pkg_msgcat->pkgpartmsgnum : '';
- },
- sub {
- my $locale = shift;
- my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
- $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
- }
- );
-
};
my $new_callback = sub {
@@ -692,8 +632,6 @@ my $new_callback = sub {
$options{'suspend_bill'}=1 if $conf->exists('part_pkg-default_suspend_bill');
- &$splice_locale_fields($fields, '', '');
-
};
my $clone_callback = sub {
@@ -732,17 +670,6 @@ my $clone_callback = sub {
foreach keys %part_pkg_currency;
}
- $recur_disabled = $object->freq ? 0 : 1;
-
- &$splice_locale_fields(
- $fields,
- '',
- sub {
- my $locale = shift;
- my $part_pkg_msgcat = $object->part_pkg_msgcat($locale);
- $part_pkg_msgcat ? $part_pkg_msgcat->pkg : '';
- }
- );
};
my $discount_error_callback = sub {
diff --git a/httemplate/edit/part_svc.cgi b/httemplate/edit/part_svc.cgi
index a07fc60..fed2125 100755
--- a/httemplate/edit/part_svc.cgi
+++ b/httemplate/edit/part_svc.cgi
@@ -36,9 +36,26 @@
}
</STYLE>
<SCRIPT TYPE="text/javascript">
+// copy all fields from the outer form (svc and its localizations, plus
+// preserve, selfservice_access, etc.) into the inner form, creating hidden
+// inputs if needed
function fixup_submit(layer) {
- document.forms[layer].submit.disabled = true;
- fixup(document.forms[layer]);
+ var layer_form = $(document.forms[layer]);
+ var main_form = $(document.forms['SvcEditMain']);
+ var data = main_form.serializeArray();
+ for (var i = 0; i < data.length; i++) {
+ var input = layer_form.children('[name=' + data[i].name + ']');
+ if (input[0]) {
+ input.prop('value', data[i].value);
+ } else {
+ $( '<input type="hidden">' )
+ .attr('name', data[i].name)
+ .prop('value', data[i].value)
+ .appendTo(layer_form);
+ }
+ }
+ layer_form[0]['submit'].disabled = true;
+ //fixup(document.forms[layer]);
window[layer+'process'].call();
}
@@ -141,19 +158,26 @@ window.onload = function() {
</SCRIPT>
-<FORM NAME="dummy">
+<FORM NAME="SvcEditMain">
<FONT CLASS="fsinnerbox-title">Service Part #<% $part_svc->svcpart ? $part_svc->svcpart : "(NEW)" %></FONT>
<TABLE CLASS="fsinnerbox">
-<TR>
- <TD ALIGN="right">Service</TD>
- <TD><INPUT TYPE="text" NAME="svc" VALUE="<% $hashref->{svc} %>"></TD>
-<TR>
+<& /elements/tr-input-locale-text.html,
+ 'object' => $part_svc,
+ 'cgi' => $cgi,
+ 'field' => 'svc',
+ 'label' => 'Service',
+ 'curr_value' => $hashref->{svc},
+&>
+%#<TR>
+%# <TD ALIGN="right">Service</TD>
+%# <TD><INPUT TYPE="text" NAME="svc" VALUE="<% $hashref->{svc} %>"></TD>
+%#<TR>
<& /elements/tr-select-part_svc_class.html, curr_value=>$hashref->{classnum} &>
<TR>
- <TD ALIGN="right">Self-service access</TD>
+ <TH ALIGN="right">Self-service access</TD>
<TD>
<SELECT NAME="selfservice_access">
% tie my %selfservice_access, 'Tie::IxHash', #false laziness w/browse/part_svc
@@ -172,12 +196,12 @@ window.onload = function() {
<TR>
- <TD ALIGN="right">Disable new orders</TD>
+ <TH ALIGN="right">Disable new orders</TD>
<TD><INPUT TYPE="checkbox" NAME="disabled" VALUE="Y"<% $hashref->{disabled} eq 'Y' ? ' CHECKED' : '' %>></TD>
</TR>
<TR>
- <TD ALIGN="right">Preserve this service on package cancellation</TD>
+ <TH ALIGN="right">Preserve this service on package cancellation</TD>
<TD><INPUT TYPE="checkbox" NAME="preserve" VALUE="Y"<% $hashref->{'preserve'} eq 'Y' ? ' CHECKED' : '' %>>&nbsp;</TD>
</TR>
@@ -240,12 +264,12 @@ my $widget = new HTML::Widgets::SelectLayers(
#'selected_layer' => $p_svcdb,
'selected_layer' => $hashref->{svcdb} || 'svc_acct',
'options' => \%svcdb,
- 'form_name' => 'dummy',
+ 'form_name' => 'SvcEditMain',
#'form_action' => 'process/part_svc.cgi',
'form_action' => 'part_svc.cgi', #self
- 'form_elements' => [qw( svc svcpart classnum selfservice_access
- disabled preserve
- )],
+# 'form_elements' => [qw( svc svcpart classnum selfservice_access
+# disabled preserve
+# )],
'html_between' => $help,
'layer_callback' => sub {
include('elements/part_svc_column.html',
diff --git a/httemplate/edit/process/elements/process.html b/httemplate/edit/process/elements/process.html
index fd12c61..60aaf74 100644
--- a/httemplate/edit/process/elements/process.html
+++ b/httemplate/edit/process/elements/process.html
@@ -62,6 +62,8 @@ Example:
'fields' => [qw( fieldname fieldname2 )],
},
+ 'process_locale' => 'fieldname', # update entries in the _msgcat table
+
'process_upload' => {
'process' => 'misc/mytable-import.html',
# fields to pass to the back end job, besides the
@@ -363,12 +365,21 @@ foreach my $value ( @values ) {
}
- if ( !$error && $opt{'process_o2m'} ) {
-
- my @process_o2m = ref($opt{'process_o2m'}) eq 'ARRAY'
- ? @{ $opt{'process_o2m'} }
- : ( $opt{'process_o2m'} );
+ my @process_o2m;
+ if ( $opt{'process_o2m'} ) {
+ @process_o2m = ref($opt{'process_o2m'}) eq 'ARRAY'
+ ? @{ $opt{'process_o2m'} }
+ : ( $opt{'process_o2m'} );
+ }
+ if ( $opt{'process_locale'} ) {
+ push @process_o2m,
+ {
+ 'table' => $table . '_msgcat',
+ 'fields' => [ 'locale', $opt{'process_locale'} ],
+ };
+ }
+ if ( !$error ) {
foreach my $process_o2m (@process_o2m) {
diff --git a/httemplate/edit/process/part_pkg.cgi b/httemplate/edit/process/part_pkg.cgi
index b804202..c4d150b 100755
--- a/httemplate/edit/process/part_pkg.cgi
+++ b/httemplate/edit/process/part_pkg.cgi
@@ -9,6 +9,7 @@
'edit_ext' => 'cgi',
'precheck_callback' => $precheck_callback,
'args_callback' => $args_callback,
+ 'process_locale' => 'pkg',
'process_m2m' => \@process_m2m,
'process_o2m' => \@process_o2m,
)
@@ -310,10 +311,6 @@ foreach my $amount_param ( grep /^usagepricepart(\d+)_amount$/, $cgi->param ) {
my @process_o2m = (
{
- 'table' => 'part_pkg_msgcat',
- 'fields' => [qw( locale pkg )],
- },
- {
'table' => 'part_pkg_usageprice',
'fields' => [qw( price currency action target amount )],
diff --git a/httemplate/elements/freeside.css b/httemplate/elements/freeside.css
index fb5e7d9..cc104a1 100644
--- a/httemplate/elements/freeside.css
+++ b/httemplate/elements/freeside.css
@@ -235,7 +235,7 @@ div.fstabcontainer {
.fsinnerbox th {
font-weight:normal;
font-size:80%;
- valign: bottom;
+ vertical-align: bottom;
color: #666666;
}
diff --git a/httemplate/elements/progress-init.html b/httemplate/elements/progress-init.html
index e38dde6..0c2b816 100644
--- a/httemplate/elements/progress-init.html
+++ b/httemplate/elements/progress-init.html
@@ -98,14 +98,14 @@ function <%$key%>process () {
overlib( 'Submitting job to server...', WIDTH, 444, HEIGHT, 168, CAPTION, 'Please wait...', STICKY, AUTOSTATUSCAP, CLOSETEXT, '', CLOSECLICK, MIDX, 0, MIDY, 0 );
+ // jQuery .serializeArray() maybe?
+ var copy_fields = <% encode_json(\%copy_fields) %>;
var Hash = new Array();
var x = 0;
var fieldName;
for (var i = 0; i<document.<%$formname%>.elements.length; i++) {
field = document.<%$formname%>.elements[i];
- if ( <% join(' || ', map { "(field.name.indexOf('$_') > -1)" } @$fields ) %>
- )
- {
+ if ( <% $all_fields %> || copy_fields[ field.name ] ) {
if ( field.type == 'select-multiple' ) {
//alert('select-multiple ' + field.name);
for (var j=0; j < field.options.length; j++) {
@@ -168,6 +168,14 @@ $progress_url->query_form(
%dest_info,
);
+my $all_fields = 0;
+my %copy_fields;
+if (grep '/^ALL$/', @$fields) {
+ $all_fields = 1;
+} else {
+ %copy_fields = map { $_ => 1 } @$fields;
+}
+
#stupid safari is caching the "location" of popup iframs, and submitting them
#instead of displaying them. this should prevent that.
my $popup_name = 'popup-'.random_id();
diff --git a/httemplate/elements/tr-input-locale-text.html b/httemplate/elements/tr-input-locale-text.html
new file mode 100644
index 0000000..110a8aa
--- /dev/null
+++ b/httemplate/elements/tr-input-locale-text.html
@@ -0,0 +1,120 @@
+<%doc>
+Usage:
+
+In edit/foo.html:
+
+<& /elements/tr-input-locale-text.html,
+ cgi => $cgi, # needed to preserve values in error redirect
+ object => $record,
+ field => 'myfield',
+ label => 'My Field',
+&>
+
+And in edit/process/foo.html:
+<& elements/process.html,
+ ...
+ process_locale => 'myfield',
+&>
+
+'object' needs to be an FS::Record subclass instance for a table that has
+a '_msgcat' localization table. For a table "foo" where "foo.myfield"
+contains some customer-visible label (in the default locale),
+"foo_msgcat.myfield" contains the translation of that label for a customer
+locale. The foreign key in foo_msgcat must have the same name as the primary
+key of foo.
+
+Currently only a single field can be localized this way; including this
+element more than once in the form will lead to conflicts. This is how
+it should work; if at some point we need to localize several fields of the
+same record, we should modify this element to show multiple inputs for each
+locale.
+
+</%doc>
+<%init>
+
+my %opt = @_;
+my $object = delete $opt{object};
+my $field = delete $opt{field};
+
+# identify our locales
+my $conf = FS::Conf->new;
+my $default_locale = $conf->config('locale') || 'en_';
+my @locales = grep { ! /^$default_locale/ } $conf->config('available-locales');
+
+my $label = delete $opt{label};
+my %labels = map { $_ => "$label&mdash;".FS::Locales->description($_) }
+ @locales;
+@locales = sort { $labels{$a} cmp $labels{$b} } @locales;
+my %curr_values;
+
+# where are the msgcat records?
+my $msgcat_table = $object->table . '_msgcat';
+my $msgcat_pkey = dbdef->table($msgcat_table)->primary_key;
+my %msgcat_pkeyvals;
+
+# find existing msgcat records, if any, and record their message values
+# and pkeys
+my $pkey = $object->primary_key;
+my $pkeyval = $object->get($pkey);
+if ($pkeyval) { # of course if this is a new record there won't be any
+ my @linked = qsearch($msgcat_table, { $pkey => $pkeyval });
+ foreach (@linked) {
+ $curr_values{ $_->locale } = $_->get( $field );
+ $msgcat_pkeyvals{ $_->locale } = $_->get( $msgcat_pkey );
+ }
+}
+
+# sticky-on-error the locale inputs
+if( my $cgi = $opt{cgi} ) {
+ my $i = 0;
+ # they're named 'foomsgnum0_locale' and 'foomsgnum0_myfield'
+ while ( my $locale = $cgi->param($msgcat_pkey . $i . '_locale') ) {
+ my $value = $cgi->param($msgcat_pkey . $i . '_' . $field);
+ $curr_values{ $locale } = $value;
+ $i++;
+ }
+}
+
+# compat with tr-input-text for styling
+my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : '';
+
+my $colspan = $opt{'colspan'} ? 'COLSPAN="'.$opt{'colspan'}.'"' : '';
+
+
+</%init>
+% # pass through %opt on all of these to retain formatting
+% # one tr, td, and input for the default locale
+<& tr-input-text.html,
+ %opt,
+ 'label' => $label,
+ 'field' => $field
+&>
+% # and one for each of the others
+% my $i = 0;
+% foreach my $locale (@locales) {
+% my $basename = $msgcat_pkey . $i;
+% my $lfield = $basename . '_' . $field;
+<& tr-td-label.html,
+ %opt,
+ 'id' => $lfield, # uniqueness
+ 'label' => $labels{$locale}
+&>
+ <TD <% $colspan %><% $cell_style %> ID="<% $lfield %>_input0">
+ <& hidden.html,
+ 'field' => $basename,
+ 'curr_value' => $msgcat_pkeyvals{$locale},
+ # will be empty if this is a new record and/or new locale, that's fine
+ &>
+ <& hidden.html,
+ 'field' => $basename . '_locale',
+ 'curr_value' => $locale,
+ &>
+ <& input-text.html,
+ %opt,
+ 'field' => $lfield,
+ 'curr_value' => $curr_values{$locale},
+ &>
+ </TD>
+</TR>
+% $i++;
+% } # foreach $locale