summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorBirte Kristina Friesel <derf@finalrewind.org>2025-07-18 19:55:02 +0200
committerBirte Kristina Friesel <derf@finalrewind.org>2025-07-18 19:55:02 +0200
commit171e797ee7f3930165606e72036d953152d5bd76 (patch)
treeb296b90a1f9d478596552fe469b5c5b833cc288f /lib
parente081fd25e5dcb01d344b99b3997c0df88f0ce133 (diff)
Add localization support.
Right now, only two languages are supported, and only a fraction of strings are already translated. There's also quite a bunch of todos left where strings are assembled in the Model, which has no access to localization functions. But that's something for iterative refinement over the next months, and (especially when it comes to adding languages and translation strings to templates) merge requests. Squashed commit of the following: commit 67d756f3bd167003907c8357126630dd7c1a3cfa Author: Birte Kristina Friesel <derf@finalrewind.org> Date: Fri Jul 18 19:53:56 2025 +0200 more translations commit 8cb0d65e70e42180419a5dd7634d332e65488dd4 Author: Birte Kristina Friesel <derf@finalrewind.org> Date: Fri Jul 18 18:54:12 2025 +0200 sme more translations commit ff12f010380914f9461966f2ef8ac6b303712ee4 Author: Birte Kristina Friesel <derf@finalrewind.org> Date: Fri Jul 18 18:53:31 2025 +0200 Add language selection to account page commit 9bf27132cbf2f87bca5af564914d96a57045ecc1 Author: Birte Kristina Friesel <derf@finalrewind.org> Date: Fri Jul 18 16:42:28 2025 +0200 Translate footer components commit 90c2c6505e933848268ed9c5bbe21e0b459cd72a Author: Birte Kristina Friesel <derf@finalrewind.org> Date: Fri Jul 18 16:16:50 2025 +0200 Use Accept-Language header if user has no preferred languages commit 814cb4a4dd4017606829ecc6b6c70822bf52a30e Author: Birte Kristina Friesel <derf@finalrewind.org> Date: Fri Jul 18 16:11:19 2025 +0200 Add list of preferred languages to user settings commit 731b789855914cb94ec091604e32aa68a678404a Author: Birte Kristina Friesel <derf@finalrewind.org> Date: Fri Jul 18 15:33:42 2025 +0200 Localization with Locale::Maketext WiP, no suitable foundation for merge requests yet. Still todo: * override Accept-Language header via account settings * Adjust all the templates and frontend javascript Related to #223
Diffstat (limited to 'lib')
-rwxr-xr-xlib/Travelynx.pm45
-rw-r--r--lib/Travelynx/Command/database.pm22
-rw-r--r--lib/Travelynx/Controller/Account.pm29
-rw-r--r--lib/Travelynx/Helper/Locales.pm22
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm2
-rw-r--r--lib/Travelynx/Model/Users.pm19
6 files changed, 134 insertions, 5 deletions
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 7dba658..33a8328 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -26,6 +26,7 @@ use Travelynx::Helper::DBRIS;
use Travelynx::Helper::EFA;
use Travelynx::Helper::HAFAS;
use Travelynx::Helper::IRIS;
+use Travelynx::Helper::Locales;
use Travelynx::Helper::MOTIS;
use Travelynx::Helper::Sendmail;
use Travelynx::Helper::Traewelling;
@@ -157,6 +158,14 @@ sub startup {
}
);
+ $self->hook(
+ 'before_render' => sub {
+ my ($self) = @_;
+
+ $self->stash( loc_handle => $self->loc_handle );
+ }
+ );
+
$self->attr(
cache_iris_main => sub {
my ($self) = @_;
@@ -412,6 +421,40 @@ sub startup {
);
$self->helper(
+ loc_handle => sub {
+ my ($self) = @_;
+
+ my @languages;
+ if ( $self->is_user_authenticated
+ and @{ $self->current_user->{languages} } )
+ {
+ @languages = @{ $self->current_user->{languages} };
+ }
+ elsif ( my $languages = $self->req->headers->accept_language ) {
+ for my $lang ( split( qr{ \s* , \s* }x, $languages ) ) {
+ if ( $lang =~ m{ ^ de }x ) {
+ push( @languages, 'de-DE' );
+ }
+ elsif ( $lang =~ m{ ^ en }x ) {
+ push( @languages, 'en-GB' );
+ }
+ }
+ }
+
+ # de-DE is our fall-back language and thus always appended
+ return Travelynx::Helper::Locales->get_handle( @languages,
+ 'de-DE' );
+ }
+ );
+
+ $self->helper(
+ 'L' => sub {
+ my ( $self, @args ) = @_;
+ $self->stash('loc_handle')->maketext(@args);
+ }
+ );
+
+ $self->helper(
'now' => sub {
return DateTime->now( time_zone => 'Europe/Berlin' );
}
@@ -3067,6 +3110,7 @@ sub startup {
$authed_r->get('/account/hooks')->to('account#webhook');
$authed_r->get('/account/traewelling')->to('traewelling#settings');
$authed_r->get('/account/insight')->to('account#insight');
+ $authed_r->get('/account/language')->to('account#change_language');
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
$authed_r->get( '/cancelled' => [ format => [ 'html', 'json' ] ] )
->to( 'traveling#cancelled', format => undef );
@@ -3097,6 +3141,7 @@ sub startup {
$authed_r->post('/account/hooks')->to('account#webhook');
$authed_r->post('/account/traewelling')->to('traewelling#settings');
$authed_r->post('/account/insight')->to('account#insight');
+ $authed_r->post('/account/language')->to('account#change_language');
$authed_r->post('/account/select_backend')->to('account#change_backend');
$authed_r->post('/checkin/add')->to('traveling#add_intransit_form');
$authed_r->post('/journey/add')->to('traveling#add_journey_form');
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm
index 34efde6..009da30 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -3359,6 +3359,28 @@ qq{select distinct checkout_station_id from in_transit where backend_id = 0;}
}
);
},
+
+ # v66 -> v67
+ # Add language settings to profile
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view users_with_backend;
+ alter table users add column language varchar(128);
+ update schema_version set version = 67;
+ create view users_with_backend as select
+ users.id as id, users.name as name, status, public_level,
+ language, email, password, registered_at, last_seen,
+ deletion_requested, deletion_notified, use_history,
+ accept_follows, notifications, profile, backend_id, iris,
+ hafas, efa, dbris, motis, backend.name as backend_name
+ from users
+ left join backends as backend on users.backend_id = backend.id
+ ;
+ }
+ );
+ },
);
sub sync_stations {
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm
index bf1eac2..8121f0a 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -874,6 +874,35 @@ sub webhook {
$self->render( 'webhooks', hook => $hook );
}
+sub change_language {
+ my ($self) = @_;
+
+ my $action = $self->req->param('action');
+ my $language = $self->req->param('language');
+
+ if ( $action and $action eq 'save' ) {
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
+ $self->users->set_language(
+ uid => $self->current_user->{id},
+ language => $language,
+ );
+ $self->flash( success => 'language' );
+ $self->redirect_to('account');
+ }
+ else {
+ my @languages = @{ $self->current_user->{languages} };
+ $self->param( language => $languages[0] // q{} );
+ $self->render('language');
+ }
+}
+
sub change_mail {
my ($self) = @_;
diff --git a/lib/Travelynx/Helper/Locales.pm b/lib/Travelynx/Helper/Locales.pm
new file mode 100644
index 0000000..12e95d1
--- /dev/null
+++ b/lib/Travelynx/Helper/Locales.pm
@@ -0,0 +1,22 @@
+package Travelynx::Helper::Locales;
+
+use strict;
+use warnings;
+
+use base qw(Locale::Maketext);
+
+our %lexicon = (
+ _AUTO => 1,
+);
+
+use Locale::Maketext::Lexicon {
+ _decode => 1,
+ '*' => [ Gettext => 'share/locales/*.po' ],
+};
+
+sub init {
+ my ($self) = @_;
+ return $self->SUPER::init( @_[ 1 .. $#_ ] );
+}
+
+1;
diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm
index b07511a..5e6195f 100755
--- a/lib/Travelynx/Model/Journeys.pm
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -50,6 +50,8 @@ sub epoch_to_dt {
);
}
+# TODO turn into a travelynx helper called from templates so that
+# loc_handle is available for localization
sub min_to_human {
my ( $self, $minutes ) = @_;
diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm
index be9e80b..3ef7f33 100644
--- a/lib/Travelynx/Model/Users.pm
+++ b/lib/Travelynx/Model/Users.pm
@@ -216,6 +216,14 @@ sub set_backend {
);
}
+sub set_language {
+ my ( $self, %opt ) = @_;
+ $opt{db} //= $self->{pg}->db;
+
+ $opt{db}
+ ->update( 'users', { language => $opt{language} }, { id => $opt{uid} } );
+}
+
sub set_privacy {
my ( $self, %opt ) = @_;
my $db = $opt{db} // $self->{pg}->db;
@@ -413,7 +421,7 @@ sub get {
my $user = $db->select(
'users_with_backend',
- 'id, name, status, public_level, email, '
+ 'id, name, status, public_level, email, language, '
. 'accept_follows, notifications, '
. 'extract(epoch from registered_at) as registered_at_ts, '
. 'extract(epoch from last_seen) as last_seen_ts, '
@@ -423,10 +431,11 @@ sub get {
)->hash;
if ($user) {
return {
- id => $user->{id},
- name => $user->{name},
- status => $user->{status},
- notifications => $user->{notifications},
+ id => $user->{id},
+ name => $user->{name},
+ languages => [ split( qr{[|]}, $user->{language} // q{} ) ],
+ status => $user->{status},
+ notifications => $user->{notifications},
accept_follows => $user->{accept_follows} == 2 ? 1 : 0,
accept_follow_requests => $user->{accept_follows} == 1 ? 1 : 0,
default_visibility => $user->{public_level} & 0x7f,