From 0f9cda3eb8afd89a9a2470d9a4c19a5c1c69b092 Mon Sep 17 00:00:00 2001 From: Jonas Smedegaard Date: Sat, 31 Aug 2024 12:24:59 +0200 Subject: streamline locale and timezone handling --- bin/events2md.pl | 65 ++++++++++++++++++++++++++++++++++++++-- lib/Object/Groupware.pm | 37 +++++++++++++++++++++++ lib/Object/Groupware/Calendar.pm | 12 ++++++-- lib/Object/Groupware/DAV.pm | 14 ++++----- lib/Object/Groupware/Event.pm | 57 ++++++++++++++++++++++++----------- 5 files changed, 155 insertions(+), 30 deletions(-) create mode 100644 lib/Object/Groupware.pm diff --git a/bin/events2md.pl b/bin/events2md.pl index e9ef842..7192cb0 100755 --- a/bin/events2md.pl +++ b/bin/events2md.pl @@ -15,6 +15,9 @@ use URI; use DateTime; use Path::Tiny; use Text::Xslate; +use POSIX qw(locale_h); # resolve LC_TIME +use locale; +use DateTime::TimeZone; use Object::Groupware::DAV; use Object::Groupware::Calendar; @@ -24,7 +27,9 @@ if ( IO::Interactive::Tiny::is_interactive() ) { } # set defaults and parse command-line options -my ( $BASE_URI, $CALENDAR_URI, $SKELDIR, $OUTPUT_FILE ); +my ($BASE_URI, $CALENDAR_URI, $SKELDIR, $OUTPUT_FILE, $CALENDAR_LANG, + $CALENDAR_TIME_ZONE, %GROUPWARE_OPTIONS +); $BASE_URI = $ENV{CAL_DAV_URL_BASE}; $CALENDAR_URI = $ENV{CAL_DAV_URL_CALENDAR}; $SKELDIR = $ENV{SKELDIR} || "$Bin/../templates"; @@ -34,6 +39,53 @@ $CALENDAR_URI ||= shift @ARGV if @ARGV; $OUTPUT_FILE = shift @ARGV if @ARGV; +$CALENDAR_LANG = $ENV{CAL_LANG} || setlocale(LC_TIME); +$CALENDAR_TIME_ZONE + = DateTime::TimeZone->new( name => ( $ENV{CAL_TIME_ZONE} || 'local' ), ); + +# extend DateTime locale with form LONGER +# * omit year and second +# * unabbreviate weekday and month +# * interpose time preposition in combined date and time, where known +my %at = ( + C => " 'at' ", + ar => " 'في' ", + da => " 'kl.' ", + de => " 'um' ", + en => " 'at' ", + es => " 'a las' ", + fr => " 'à' ", + he => " 'בשעה' ", + it => " 'alle' ", + ja => "'に'", + no => " 'kl.' ", + ru => " 'в' ", + zh => "'在'", +); +my $dt_locale = DateTime::Locale->load($CALENDAR_LANG); +my ( $locale, $lang ) = $dt_locale->code =~ /^((\w+)(?:-\w+)?)/; +my $dt = DateTime->now( locale => $dt_locale ); +my %dt_locale_data = $dt_locale->locale_data; +$dt_locale_data{code} = "${locale}-LONGER"; +$dt_locale_data{name} .= ' nouns unabbreviated'; +$dt_locale_data{date_format_medium} = $dt->locale->format_for('MMMMEd'); +$dt_locale_data{date_format_medium} ||= $dt->locale->format_for('MMMEd'); +$dt_locale_data{date_format_medium} =~ s/\bMMM\b/MMMM/; +$dt_locale_data{date_format_medium} =~ s/\bMMM\b/MMMM/; +$dt_locale_data{date_format_medium} =~ s/\bE\b/EEEE/; +$dt_locale_data{time_format_medium} = $dt->locale->format_for('Hm'); +$dt_locale_data{datetime_format_medium} + =~ s/^\{1\}\K,? (?=\{0\}$)/$at{$lang}/ + if $at{$lang}; +%GROUPWARE_OPTIONS = ( + dt_locale => DateTime::Locale::FromData->new( \%dt_locale_data ), + dt_time_zone => $CALENDAR_TIME_ZONE, +); +$log->infof( + 'Will use locale %s and time zone %s', + $GROUPWARE_OPTIONS{dt_locale}->code, + $GROUPWARE_OPTIONS{dt_time_zone}->name, +); # resolve calendar URIs my ( $base_uri, $calendar_uri, $calendar ); @@ -56,6 +108,7 @@ if ( $base_uri->scheme eq 'http' or $base_uri->scheme eq 'https' ) { user => $ENV{CAL_DAV_USER}, pass => $ENV{CAL_DAV_PASS}, uri => $base_uri, + %GROUPWARE_OPTIONS, ); $calendar = $session->get($calendar_uri); } @@ -68,12 +121,18 @@ elsif ( $base_uri->scheme eq 'file' ) { $log->debug('parse local calendar data...'); my $path = path( $base_uri->file ); if ( $path->is_file ) { - $calendar = Object::Groupware::Calendar->new( filename => "$path" ); + $calendar = Object::Groupware::Calendar->new( + filename => "$path", + %GROUPWARE_OPTIONS, + ); } else { my $data; $path->visit( sub { $data .= $_->slurp_raw if $_->is_file } ); - $calendar = Object::Groupware::Calendar->new( data => $data ); + $calendar = Object::Groupware::Calendar->new( + data => $data, + %GROUPWARE_OPTIONS, + ); } } if ( $log->is_trace ) { diff --git a/lib/Object/Groupware.pm b/lib/Object/Groupware.pm new file mode 100644 index 0000000..083f415 --- /dev/null +++ b/lib/Object/Groupware.pm @@ -0,0 +1,37 @@ +use v5.36; +use Feature::Compat::Class 0.07; + +package Object::Groupware 0.01; + +class Object::Groupware; + +use utf8; + +use Log::Any qw( ); + +field $log = Log::Any->get_logger; + +field $dt_locale : param : reader = undef; +field $dt_time_zone : param : reader = undef; +field $dt_span_time_prefix : param : reader = ''; + +ADJUST { + if ( defined $self->dt_locale ) { + $dt_locale = DateTime::Locale->load( $self->dt_locale ) + unless $self->dt_locale isa DateTime::Locale::FromData; + $log->debugf( + 'Class %s set up to use locale %s (%s)', + __CLASS__, $self->dt_locale->code, $self->dt_locale->name + ); + } + if ( defined $self->dt_time_zone ) { + $dt_time_zone = DateTime::TimeZone->new( name => $self->dt_time_zone ) + unless $self->dt_time_zone isa DateTime::TimeZone; + $log->debugf( + 'Class %s set up to use time zone %s', + __CLASS__, $self->dt_time_zone->name + ); + } +} + +1; diff --git a/lib/Object/Groupware/Calendar.pm b/lib/Object/Groupware/Calendar.pm index 7226782..386a749 100644 --- a/lib/Object/Groupware/Calendar.pm +++ b/lib/Object/Groupware/Calendar.pm @@ -3,7 +3,7 @@ use Feature::Compat::Class 0.07; package Object::Groupware::Calendar 0.01; -class Object::Groupware::Calendar; +class Object::Groupware::Calendar : isa(Object::Groupware); use utf8; @@ -39,7 +39,15 @@ method events ( $set = undef, $period = undef ) $set->start, $set->end ) if $set; - my @events = map { Object::Groupware::Event->new( entry => $_ ) } sort { + my $dt_locale = $self->dt_locale; + my $dt_time_zone = $self->dt_time_zone; + my @events = map { + Object::Groupware::Event->new( + entry => $_, + dt_locale => $dt_locale, + dt_time_zone => $dt_time_zone, + ) + } sort { $a->start->compare( $b->start ) || $a->start->compare( $b->start ) || $a->summary cmp $b->summary diff --git a/lib/Object/Groupware/DAV.pm b/lib/Object/Groupware/DAV.pm index 5c191e3..5dc789d 100644 --- a/lib/Object/Groupware/DAV.pm +++ b/lib/Object/Groupware/DAV.pm @@ -3,13 +3,12 @@ use Feature::Compat::Class 0.07; package Object::Groupware::DAV 0.01; -class Object::Groupware::DAV; +class Object::Groupware::DAV : isa(Object::Groupware); use utf8; use Feature::Compat::Try; -use POSIX qw(locale_h); # resolve LC_TIME -use locale; + use Net::Netrc; use IO::Interactive::Tiny; use Log::Any qw( ); @@ -20,9 +19,6 @@ use DateTime; use Object::Groupware::Calendar; -# use system locale to format DateTime objects parsed from iCal data -DateTime->DefaultLocale( setlocale(LC_TIME) ); - field $log = Log::Any->get_logger; field $uri : param; @@ -80,7 +76,11 @@ method get ($new_uri) # 3. PROPFIND with depth: 1 # as documented at - return Object::Groupware::Calendar->new( data => $session->cal ); + return Object::Groupware::Calendar->new( + data => $session->cal, + dt_locale => $self->dt_locale, + dt_time_zone => $self->dt_time_zone, + ); } 1; diff --git a/lib/Object/Groupware/Event.pm b/lib/Object/Groupware/Event.pm index 52fa143..c5ccb6e 100644 --- a/lib/Object/Groupware/Event.pm +++ b/lib/Object/Groupware/Event.pm @@ -3,50 +3,71 @@ use Feature::Compat::Class 0.07; package Object::Groupware::Event 0.01; -class Object::Groupware::Event; +class Object::Groupware::Event : isa(Object::Groupware); use utf8; use Log::Any qw( ); use Feature::Compat::Try; +use DateTime::Locale; field $log = Log::Any->get_logger; field $entry : param; +field $dt_locale; -field $begin : reader = $entry->start; -field $date_begin : reader = $begin->strftime('%A %e. %B'); -field $time_begin : reader = $begin->strftime('%k.%M'); -field $end : reader = $entry->end; -field $date_end : reader; -field $time_end : reader; +field $begin : reader = $entry->start; +field $end : reader = $entry->end; field $datespan : reader; field $timespan : reader; field $time_brief : reader; field $summary : reader = $entry->summary; -field $description : reader = $entry->description; +field $description : reader = $entry->description || ''; field $location : reader = $entry->_simple_property('location'); field $price : reader; field @attendees; field @attachments; ADJUST { - if ( defined $end ) { - $date_end = $end->strftime('%A %e. %B'); - $time_end = $end->strftime('%k.%M'); + if ( $self->dt_locale ) { + $dt_locale = $self->dt_locale; + $begin->set_locale($dt_locale); + $end->set_locale($dt_locale) if defined $end; + } + else { + + # we need the object regardless, to fetch CLDR patterns + $dt_locale = DateTime::Locale->load('en_US'); + } + + $begin->set_locale( $self->dt_locale ); + + for ( $self->dt_time_zone || () ) { + $begin->set_time_zone($_); + $end->set_time_zone($_) if defined $end; } $datespan - = ( defined $end and $date_end ne $date_begin ) - ? ucfirst("$date_begin - $date_end") - : ucfirst("$date_begin"); - $timespan - = ( defined $end and not $entry->all_day ) - ? ucfirst("$date_begin kl. $time_begin-$time_end") + = ( defined $end + and $end->clone->truncate( to => 'day' ) ne + $begin->clone->truncate( to => 'day' ) ) + ? ucfirst( + sprintf '%s - %s', + $begin->format_cldr( $dt_locale->date_format_medium() ), + $end->format_cldr( $dt_locale->date_format_medium() ) + ) + : ucfirst( $begin->format_cldr( $dt_locale->date_format_medium() ) ); + $timespan = ( defined $end and not $entry->all_day ) + ? ucfirst( + sprintf '%s-%s', + $begin->format_cldr( $dt_locale->datetime_format_medium() ), + $end->format_cldr( $dt_locale->time_format_medium() ) + ) : undef; $time_brief = $entry->all_day ? $datespan - : ucfirst("$date_begin kl. $time_begin"); + : ucfirst( + $begin->format_cldr( $dt_locale->datetime_format_medium() ) ); $description =~ s/\n\n[Pp]ris:\s*((?!\n).+)\s*\z//m; $price = $1; -- cgit v1.2.3