1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
|
package Zonemaster::Engine::Translator;
use v5.16.0;
use warnings;
use version; our $VERSION = version->declare("v1.0.8");
use Carp qw[confess croak];
use Locale::Messages qw[textdomain];
use Locale::TextDomain qw[Zonemaster-Engine];
use POSIX qw[setlocale LC_MESSAGES];
use Readonly;
use Zonemaster::Engine::Test;
###
### Tag descriptions
###
Readonly my %TAG_DESCRIPTIONS => (
CANNOT_CONTINUE => sub {
__x # SYSTEM:CANNOT_CONTINUE
"Not enough data about {domain} was found to be able to run tests.", @_;
},
DEPENDENCY_VERSION => sub {
__x # SYSTEM:DEPENDENCY_VERSION
"Using prerequisite module {name} version {version}.", @_;
},
GLOBAL_VERSION => sub {
__x # SYSTEM:GLOBAL_VERSION
"Using version {version} of the Zonemaster engine.", @_;
},
LOGGER_CALLBACK_ERROR => sub {
__x # SYSTEM:LOGGER_CALLBACK_ERROR
"Logger callback died with error: {exception}", @_;
},
LOOKUP_ERROR => sub {
__x # SYSTEM:LOOKUP_ERROR
"DNS query to {ns} for {domain}/{type}/{class} failed with error: {message}", @_;
},
MODULE_ERROR => sub {
__x # SYSTEM:MODULE_ERROR
"Fatal error in {module}: {msg}", @_;
},
MODULE_VERSION => sub {
__x # SYSTEM:MODULE_VERSION
"Using module {module} version {version}.", @_;
},
MODULE_END => sub {
__x # SYSTEM:MODULE_END
"Module {module} finished running.", @_;
},
NO_NETWORK => sub {
__x # SYSTEM:NO_NETWORK
"Both IPv4 and IPv6 are disabled.";
},
UNKNOWN_METHOD => sub {
__x # SYSTEM:UNKNOWN_METHOD
"Request to run unknown method {testcase} in module {module}.", @_;
},
UNKNOWN_MODULE => sub {
__x # SYSTEM:UNKNOWN_MODULE
"Request to run {testcase} in unknown module {module}. Known modules: {module_list}.", @_;
},
SKIP_IPV4_DISABLED => sub {
__x # SYSTEM:SKIP_IPV4_DISABLED
"IPv4 is disabled, not sending \"{rrtype}\" query to {ns}.", @_;
},
SKIP_IPV6_DISABLED => sub {
__x # SYSTEM:SKIP_IPV6_DISABLED
"IPv6 is disabled, not sending \"{rrtype}\" query to {ns}.", @_;
},
FAKE_DELEGATION_TO_SELF => sub {
__x # SYSTEM:FAKE_DELEGATION_TO_SELF
"Name server {ns} not adding fake delegation for domain {domain} to itself.", @_;
},
FAKE_DELEGATION_IN_ZONE_NO_IP => sub {
__x # SYSTEM:FAKE_DELEGATION_IN_ZONE_NO_IP
"The fake delegation of domain {domain} includes an in-zone name server {nsname} "
. "without mandatory glue (without IP address).",
@_;
},
FAKE_DELEGATION_NO_IP => sub {
__x # SYSTEM:FAKE_DELEGATION_NO_IP
"The fake delegation of domain {domain} includes a name server {nsname} "
. "that cannot be resolved to any IP address.",
@_;
},
PACKET_BIG => sub {
__x # SYSTEM:PACKET_BIG
"Big packet size ({size}) (try with \"{command}\").", @_;
},
);
###
### Construction
###
my $instance;
sub new {
my ( $class, %attrs ) = @_;
$class->initialize( %attrs );
return $class->instance;
}
sub instance {
my ( $class ) = @_;
if ( !defined $instance ) {
$class->initialize();
}
return $instance;
}
sub initialize {
my ( $class, %attrs ) = @_;
if ( defined $instance ) {
confess "already initialized";
}
my $locale;
if ( exists $attrs{locale} ) {
$locale = delete $attrs{locale};
if ( !defined $locale || ref $locale ne '' ) {
confess "argument 'locale' must not be a defined scalar";
}
}
my $obj = {
_locale => $locale // _init_locale(),
_all_tag_descriptions => $class->_build_all_tag_descriptions(),
_last_language => _build_last_language(),
};
$instance = bless $obj, $class;
return;
}
###
### Builder Methods
###
# Get the program's underlying LC_MESSAGES and make sure it can be effectively
# updated down the line.
#
# If the underlying LC_MESSAGES is invalid, it attempts to second guess Perl's
# fallback locale.
#
# Side effects:
# * Updates the program's underlying LC_MESSAGES to the returned value.
# * Unsets LC_ALL.
sub _init_locale {
my $locale = setlocale( LC_MESSAGES, "" );
delete $ENV{LC_ALL};
if ( !defined $locale ) {
my $language = $ENV{LANGUAGE} // "";
for my $value ( split /:/, $language ) {
if ( $value ne "" && $value !~ /[.]/ ) {
$value .= ".UTF-8";
}
$locale = setlocale( LC_MESSAGES, $value );
if ( defined $locale ) {
last;
}
}
$locale //= "C";
}
return $locale;
}
sub _load_data {
my $self = shift;
my $old_locale = $self->locale;
$self->locale( 'C' );
my %data;
for my $mod ( keys %{ $self->all_tag_descriptions } ) {
for my $tag ( keys %{ $self->all_tag_descriptions->{$mod} } ) {
$data{$mod}{$tag} = $self->_translate_tag( $mod, $tag, {} );
}
}
$self->locale( $old_locale );
return \%data;
}
sub _build_all_tag_descriptions {
my ( $class ) = @_;
my %all_tag_descriptions;
$all_tag_descriptions{System} = \%TAG_DESCRIPTIONS;
foreach my $mod ( Zonemaster::Engine::Test->modules ) {
my $module = 'Zonemaster::Engine::Test::' . $mod;
$all_tag_descriptions{ $mod } = $module->tag_descriptions;
}
return \%all_tag_descriptions;
}
sub _build_last_language {
return $ENV{LANGUAGE} // '';
}
###
### Instance methods
###
sub data {
my ( $self ) = @_;
if ( !exists $self->{_data} ) {
$self->{_data} = $self->_load_data;
}
return $self->{_data};
}
sub all_tag_descriptions {
my ( $self ) = @_;
return $self->{_all_tag_descriptions};
}
sub locale {
my ( $self, @args ) = @_;
if ( @args ) {
my $new_locale = shift @args;
# On some systems gettext takes its locale from setlocale().
if ( !defined setlocale( LC_MESSAGES, $new_locale ) ) {
return;
}
$self->_last_language( $ENV{LANGUAGE} // '' );
# On some systems gettext takes its locale from %ENV.
$ENV{LC_MESSAGES} = $new_locale;
# On some systems gettext refuses to switch over to another locale unless
# the textdomain is reset.
textdomain( 'Zonemaster-Engine' );
if ( !defined $new_locale || ref $new_locale ne '' ) {
croak "locale must be a defined scalar";
}
$self->{_locale} = $new_locale;
} ## end if ( @args )
return $self->{_locale};
};
sub to_string {
my ( $self, $entry ) = @_;
return sprintf( "%7.2f %-9s %s", $entry->timestamp, $entry->level, $self->translate_tag( $entry ) );
}
sub translate_tag {
my ( $self, $entry ) = @_;
return $self->_translate_tag( $entry->module, $entry->tag, $entry->printable_args ) // $entry->string;
}
sub test_case_description {
my ( $self, $test_name ) = @_;
my $module = $test_name;
$module =~ s/\d+$//;
return $self->_translate_tag( $module, uc $test_name, {} ) // $test_name;
}
sub _last_language {
my $self = shift;
if ( @_ ) {
my $last_language = shift;
if ( !defined $last_language || ref $last_language ne '' ) {
croak "_last_language must be a defined scalar";
}
$self->{_last_language} = $last_language;
}
return $self->{_last_language};
}
sub _translate_tag {
my ( $self, $module, $tag, $args ) = @_;
if ( $ENV{LANGUAGE} // '' ne $self->_last_language ) {
$self->locale( $self->locale );
}
my $code = $self->all_tag_descriptions->{$module}{$tag};
if ( $code ) {
return $code->( %{$args} );
}
else {
return undef;
}
}
1;
=head1 NAME
Zonemaster::Engine::Translator - translation support for Zonemaster
=head1 SYNOPSIS
Zonemaster::Engine::Translator->initialize( locale => 'sv_SE.UTF-8' );
my $trans = Zonemaster::Engine::Translator->instance;
say $trans->to_string($entry);
This is a singleton class.
The instance of this class requires exclusive control over C<$ENV{LC_MESSAGES}>
and the program's underlying LC_MESSAGES.
At times it resets gettext's textdomain.
On construction it unsets C<$ENV{LC_ALL}> and from then on it must remain unset.
On systems that support C<$ENV{LANGUAGE}>, this variable overrides the locale()
attribute unless the locale() attribute is set to C<"C">.
=head1 ATTRIBUTES
=over
=item locale
The locale used for localized messages.
say $translator->locale();
if ( !$translator->locale( 'sv_SE.UTF-8' ) ) {
say "failed to update locale";
}
The value of this attribute is mirrored in C<$ENV{LC_MESSAGES}>.
When writing to this attribute, a request is made to update the program's
underlying LC_MESSAGES.
If this request fails, the attribute value remains unchanged and an empty list
is returned.
As a side effect when successfully updating this attribute gettext's textdomain
is reset.
=item data
A reference to a hash with translation data. This is unlikely to be useful to
end-users.
=item all_tag_descriptions
=back
=head1 METHODS
=over
=item initialize(%args)
Provide initial values for the single instance of this class.
Zonemaster::Engine::Translator->initialize( locale => 'sv_SE.UTF-8' );
This method must be called at most once and before the first call to instance().
=item instance()
Returns the single instance of this class.
my $translator = Zonemaster::Engine::Translator->instance;
If initialize() has not been called prior to the first call to instance(), it
is the same as if initialize() had been called without arguments.
=item new(%args)
Use of this method is deprecated.
=over
=item locale
If no initial value is provided to the constructor, one is determined by calling
setlocale( LC_MESSAGES, "" ).
=back
=item to_string($entry)
Takes a L<Zonemaster::Engine::Logger::Entry> object as its argument and returns a translated string with the timestamp, level, message and arguments in the
entry.
=item translate_tag($entry)
Takes a L<Zonemaster::Engine::Logger::Entry> object as its argument and returns a translation of its tag and arguments.
=item test_case_description($testcase)
Takes a string (test case ID) and returns the translated test case description.
=item BUILD
Internal method that's only mentioned here to placate L<Pod::Coverage>.
=back
=cut
|