diff options
author | Birte Kristina Friesel <derf@finalrewind.org> | 2025-07-18 19:55:02 +0200 |
---|---|---|
committer | Birte Kristina Friesel <derf@finalrewind.org> | 2025-07-18 19:55:02 +0200 |
commit | 171e797ee7f3930165606e72036d953152d5bd76 (patch) | |
tree | b296b90a1f9d478596552fe469b5c5b833cc288f | |
parent | e081fd25e5dcb01d344b99b3997c0df88f0ce133 (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
-rw-r--r-- | cpanfile | 2 | ||||
-rwxr-xr-x | lib/Travelynx.pm | 45 | ||||
-rw-r--r-- | lib/Travelynx/Command/database.pm | 22 | ||||
-rw-r--r-- | lib/Travelynx/Controller/Account.pm | 29 | ||||
-rw-r--r-- | lib/Travelynx/Helper/Locales.pm | 22 | ||||
-rwxr-xr-x | lib/Travelynx/Model/Journeys.pm | 2 | ||||
-rw-r--r-- | lib/Travelynx/Model/Users.pm | 19 | ||||
-rw-r--r-- | share/locales/de_DE.po | 198 | ||||
-rw-r--r-- | share/locales/en_GB.po | 180 | ||||
-rw-r--r-- | templates/_checked_in.html.ep | 8 | ||||
-rw-r--r-- | templates/_public_status_card.html.ep | 14 | ||||
-rw-r--r-- | templates/_wagons.html.ep | 8 | ||||
-rw-r--r-- | templates/account.html.ep | 37 | ||||
-rw-r--r-- | templates/landingpage.html.ep | 4 | ||||
-rw-r--r-- | templates/language.html.ep | 36 | ||||
-rw-r--r-- | templates/layouts/default.html.ep | 14 | ||||
-rw-r--r-- | templates/login.html.ep | 10 | ||||
-rw-r--r-- | templates/register.html.ep | 18 |
18 files changed, 611 insertions, 57 deletions
@@ -9,6 +9,8 @@ requires 'GIS::Distance::Fast'; requires 'IO::Socket::Socks', '>= 0.64'; requires 'IO::Socket::SSL', '>= 2.009'; requires 'List::UtilsBy'; +requires 'Locale::Maketext'; +requires 'Locale::Maketext::Lexicon'; requires 'Math::Polygon'; requires 'MIME::Entity'; requires 'Mojolicious'; 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, diff --git a/share/locales/de_DE.po b/share/locales/de_DE.po new file mode 100644 index 0000000..7d86a42 --- /dev/null +++ b/share/locales/de_DE.po @@ -0,0 +1,198 @@ +msgid "" +msgstr "" +"Language: de-DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +# +# Global Strings +# + +msgid "button.register" +msgstr "Registrieren" + +msgid "button.login" +msgstr "Anmelden" + +msgid "button.logout" +msgstr "Abmelden" + +msgid "footer.imprint" +msgstr "Impressum" + +msgid "footer.privacy" +msgstr "Datenschutz" + +msgid "footer.legend" +msgstr "Legende" + +msgid "footer.colour-scheme" +msgstr "Farbschema" + +msgid "footer.colour-scheme.light" +msgstr "hell" + +msgid "footer.colour-scheme.dark" +msgstr "dunkel" + +msgid "footer.colour-scheme.auto" +msgstr "automatisch" + +# +# Templates +# + +# account.html.ep + +msgid "account.account" +msgstr "Account" + +msgid "account.name" +msgstr "Name" + +msgid "account.mail" +msgstr "E-Mail" + +msgid "account.password" +msgstr "Passwort" + +msgid "account.language" +msgstr "Sprache" + +msgid "account.connections" +msgstr "Verbindungen" + +msgid "account.connections.enabled" +msgstr "Vorschläge aktiv" + +msgid "account.connections.disabled" +msgstr "Vorschläge deaktiviert" + +msgid "account.visibility" +msgstr "Sichtbarkeit" + +msgid "account.interaction" +msgstr "Interaktion" + +msgid "account.interaction.accept-follows" +msgstr "Accounts können dir direkt folgen" + +msgid "account.interaction.accept-follow-requests" +msgstr "Accounts können dir auf Anfrage folgen" + +msgid "account.interaction.one" +msgstr "eine" + +msgid "account.interaction.open-request" +msgstr "offene Anfrage" + +msgid "account.interaction.open-requests" +msgstr "offene Anfragen" + +msgid "account.interaction.disabled" +msgstr "Accounts können dir nicht folgen" + +# login.html.ep + +msgid "login.accept-tos-pre" +msgstr "Mit der Anmeldung stimmst du den" + +msgid "login.tos" +msgstr "Nutzungsbedingungen" + +msgid "login.accept-tos-post" +msgstr "zu." + +msgid "login.forgot-password" +msgstr "Passwort vergessen" + +msgid "login.registration-disabled" +msgstr "Diese Instanz erlaubt derzeit keine Registrierung neuer Accounts" + +# register.html.ep + +msgid "register.name" +msgstr "Name (alphanumerisch)" + +msgid "register.mail" +msgstr "E-Mail-Adresse" + +msgid "register.password" +msgstr "Passwort" + +msgid "register.repeat-password" +msgstr "Passwort wiederholen" + +msgid "register.accept-tos-pre" +msgstr "Mit deiner Registrierung stimmst du den" + +msgid "register.tos" +msgstr "Nutzungsbedingungen" + +msgid "register.accept-tos-post" +msgstr "zu." + +msgid "register.expect-confirmation-link" +msgstr "Nach der Registrierung wird ein für 48 Stunden gültiger Bestätigungslink an die angegebene Mail-Adresse geschickt. Eine Anmeldung ist erst nach Bestätigung der Mail-Adresse möglich." + +msgid "register.why-mail" +msgstr "Die Mail-Adresse wird ausschließlich zur Bestätigung der Anmeldung, für die „Passwort vergessen“-Funktionalität und für wichtige Informationen über den Account verwendet und nicht an Dritte weitergegeben." + +msgid "register.privacy-pre" +msgstr "Die" + +msgid "register.privacy" +msgstr "Datenschutzerklärung" + +msgid "register.privacy-post" +msgstr "beschreibt weitere erhobene Daten sowie deren Zweck und Speicherfristen." + +msgid "register.account-deletion" +msgstr "Accounts werden nach einem Jahr ohne Aktivität per E-Mail über die bevorstehende Löschung informiert und nach vier weiteren Wochen ohne Aktivität automatisch gelöscht." + +msgid "register.disclaimer" +msgstr "Bitte beachten: Travelynx ist ein privat betriebenes Projekt ohne Verfügbarkeitsgarantie. Unangekündigte Downtimes oder eine kurzfristige Einstellung dieser Seite sind nicht vorgesehen, aber möglich." + +# _public_status_card.html.ep + +msgid "status.is-checked-in" +msgstr "ist unterwegs" + +msgid "status.is-not-checked-in" +msgstr "ist gerade nicht eingecheckt" + +msgid "status.share" +msgstr "Teilen" + +msgid "status.arrival-in" +msgstr "Ankunft in" + +msgid "status.arrival-soon" +msgstr "Ankunft in weniger als einer Minute" + +msgid "status.arrival-unknown" +msgstr "Ankunft unbekannt" + +msgid "status.arrived" +msgstr "Ziel erreicht" + +msgid "status.carriages" +msgstr "Wagen" + +msgid "status.route" +msgstr "Route" + +# _wagons.html.ep + +msgid "wagons.name-as-type" +msgstr "als" + +msgid "wagons.from" +msgstr "von" + +msgid "wagons.to" +msgstr "nach" + +msgid "wagons.carriage" +msgstr "Wagen" diff --git a/share/locales/en_GB.po b/share/locales/en_GB.po new file mode 100644 index 0000000..47983a3 --- /dev/null +++ b/share/locales/en_GB.po @@ -0,0 +1,180 @@ +msgid "" +msgstr "" +"Language: en-GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +# +# Global Strings +# + +msgid "button.register" +msgstr "Register" + +msgid "button.login" +msgstr "Login" + +msgid "button.logout" +msgstr "Logout" + +msgid "footer.imprint" +msgstr "Imprint" + +msgid "footer.privacy" +msgstr "Privacy" + +msgid "footer.legend" +msgstr "Legend" + +msgid "footer.colour-scheme" +msgstr "Display Mode" + +msgid "footer.colour-scheme.light" +msgstr "light" + +msgid "footer.colour-scheme.dark" +msgstr "dark" + +msgid "footer.colour-scheme.auto" +msgstr "auto" + +# +# Templates +# + +# account.html.ep + +msgid "account.account" +msgstr "Account" + +msgid "account.name" +msgstr "Name" + +msgid "account.mail" +msgstr "E-Mail" + +msgid "account.password" +msgstr "Password" + +msgid "account.language" +msgstr "Language" + +msgid "account.connections" +msgstr "Connections" + +msgid "account.connections.enabled" +msgstr "Suggestions enabled" + +msgid "account.connections.disabled" +msgstr "Suggestions disabled" + +msgid "account.visibility" +msgstr "Visibility" + +msgid "account.interaction" +msgstr "Interaction" + +msgid "account.interaction.accept-follows" +msgstr "Accounts may follow you" + +msgid "account.interaction.accept-follow-requests" +msgstr "Accounts may send follow requests" + +msgid "account.interaction.one" +msgstr "one" + +msgid "account.interaction.open-request" +msgstr "open request" + +msgid "account.interaction.open-requests" +msgstr "open requests" + +msgid "account.interaction.disabled" +msgstr "Accounts cannot follow you" + +# login.html.ep + +msgid "login.tos" +msgstr "terms of use" + +msgid "login.accept-tos-pre" +msgstr "By logging in, you accept the" + +msgid "login.accept-tos-post" +msgstr " " + +msgid "login.forgot-password" +msgstr "Forgot password" + +msgid "login.registration-disabled" +msgstr "This instance does not allow registration of new accounts at the moment" + +# register.html.ep + +msgid "register.name" +msgstr "Name (alphanumeric)" + +msgid "register.mail" +msgstr "E-Mail address" + +msgid "register.password" +msgstr "Password" + +msgid "register.repeat-password" +msgstr "Repeat password" + +msgid "register.tos" +msgstr "terms of use" + +msgid "register.accept-tos-pre" +msgstr "By submitting this registration form, you accept the" + +msgid "register.accept-tos-post" +msgstr " " + +msgid "register.expect-confirmation-link" +msgstr "After submitting the registration, a confirmation link will be sent to the provided E-Mail address. Logging into the new travelynx account is only possible after following that link. The link is valid for 48 hours." + +# _checked_in, _public_status_card.html.ep + +msgid "status.is-checked-in" +msgstr "is in transit" + +msgid "status.is-not-checked-in" +msgstr "ist not in transit right now" + +msgid "status.share" +msgstr "Share" + +msgid "status.arrival-in" +msgstr "Arrival in" + +msgid "status.arrival-soon" +msgstr "Arrival in less than one minute" + +msgid "status.arrival-unknown" +msgstr "Arrival unknown" + +msgid "status.arrived" +msgstr "Arrived" + +msgid "status.carriages" +msgstr "Carriages" + +msgid "status.route" +msgstr "Route" + +# _wagons.html.ep + +msgid "wagons.name-as-type" +msgstr "running as" + +msgid "wagons.from" +msgstr "from" + +msgid "wagons.to" +msgstr "towards" + +msgid "wagons.carriage" +msgstr "Carriage" diff --git a/templates/_checked_in.html.ep b/templates/_checked_in.html.ep index e1cefe6..c798393 100644 --- a/templates/_checked_in.html.ep +++ b/templates/_checked_in.html.ep @@ -32,13 +32,13 @@ % } % elsif (defined $journey->{arrival_countdown}) { % if ($journey->{arrival_countdown} > 60) { - Ankunft in <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %> + <%= L('status.arrival-in') %> <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %> % } % elsif ($journey->{arrival_countdown} > 0) { - Ankunft in weniger als einer Minute + %= L('status.arrival-soon') % } % else { - Ziel erreicht + %= L('status.arrived') % } % if ($journey->{arrival_countdown} < (60 * 15) and $journey->{arr_platform}) { % if ($journey->{arr_direction} and $journey->{arr_direction} eq 'r') { @@ -311,7 +311,7 @@ data-url="<%= url_for('/status')->to_abs->scheme('https') %>/<%= $user->{name} %>/<%= $journey->{sched_departure}->epoch %>?token=<%= $journey->{dep_eva} %>-<%= $journey->{timestamp}->epoch % 337 %>" % } > - <i class="material-icons left" aria-hidden="true">share</i> Teilen + <i class="material-icons left" aria-hidden="true">share</i> <%= L('status.share') %> </a> % } % else { diff --git a/templates/_public_status_card.html.ep b/templates/_public_status_card.html.ep index 32b193a..73840b3 100644 --- a/templates/_public_status_card.html.ep +++ b/templates/_public_status_card.html.ep @@ -11,7 +11,7 @@ <a href="/status/<%= $name %>"><%= $name %></a>: <%= include '_format_train', journey => $journey %> % } % else { - <a href="/p/<%= $name %>"><%= $name %></a> ist unterwegs + <a href="/p/<%= $name %>"><%= $name %></a> <%= L('status.is-checked-in') %> % } <i class="material-icons right"><%= visibility_icon($journey->{effective_visibility_str}) %></i> % if (not $journey->{extra_data}{rt}) { @@ -41,10 +41,10 @@ % } % elsif (defined $journey->{arrival_countdown}) { % if ($journey->{arrival_countdown} > 60) { - Ankunft in <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %> + <%= L('status.arrival-in') %> <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %> % } % elsif ($journey->{arrival_countdown} > 0) { - Ankunft in weniger als einer Minute + %= L('status.arrival-soon') % } % else { Ziel erreicht @@ -54,7 +54,7 @@ % } % } % elsif ($journey->{arr_name}) { - Ankunft in mehr als zwei Stunden + %= L('status.arrival-unknown') % } </div> <div class="progress" style="height: 1ex;"> @@ -216,14 +216,14 @@ % } % else { <div class="wagons" style="margin-top: 2ex;"> - Wagen:<br/> + <%= L('status.carriages') %>:<br/> %= include '_wagons', wagongroups => $journey->{wagongroups}; </div> % } % } % if (not stash('from_timeline')) { <div style="margin-top: 2ex;"> - Route:<br/> + <%= L('status.route') %>:<br/> % my $before = 1; % my $within = 0; % my $at_startstop = 0; @@ -280,7 +280,7 @@ <span class="card-title">Aktuell nicht eingecheckt</span> % } % else { - <span class="card-title"><a href="/p/<%= $name %>"><%= $name %></a> ist gerade nicht eingecheckt</span> + <span class="card-title"><a href="/p/<%= $name %>"><%= $name %></a> <%= L('status.is-not-checked-in') %></span> % } <div> % if ($journey->{arr_name}) { diff --git a/templates/_wagons.html.ep b/templates/_wagons.html.ep index 4090f11..926aac1 100644 --- a/templates/_wagons.html.ep +++ b/templates/_wagons.html.ep @@ -7,12 +7,12 @@ % elsif ($wagon_number and my $group_name = app->ice_name->{$wagon_number}) { „<%= $group_name %>“ % } - als <b><%= $wagongroup->{type} // $journey->{type} %> <%= $wagongroup->{no} %></b> + <%= L('wagons.name-as-type') %> <b><%= $wagongroup->{type} // $journey->{type} %> <%= $wagongroup->{no} %></b> % if ($wagongroup->{from}) { - von <b><%= $wagongroup->{from} %></b> + <%= L('wagons.from') %> <b><%= $wagongroup->{from} %></b> % } % if ($wagongroup->{to}) { - nach <b><%= $wagongroup->{to} %></b> + <%= L('wagons.to') %> <b><%= $wagongroup->{to} %></b> % } <br/> % for my $wagon (@{$wagongroup->{wagons}}) { @@ -24,7 +24,7 @@ % } %= $wagon->{type} % if ($wagon->{number}) { - – Wagen <%= $wagon->{number} %> + – <%= L('wagons.carriage') %> <%= $wagon->{number} %> % } <br/> % } diff --git a/templates/account.html.ep b/templates/account.html.ep index e4bf38d..837f219 100644 --- a/templates/account.html.ep +++ b/templates/account.html.ep @@ -16,6 +16,9 @@ % elsif ($success eq 'password') { <span class="card-title">Passwort geändert</span> % } + % elsif ($success eq 'language') { + <span class="card-title">Sprache geändert</span> + % } % elsif ($success eq 'privacy') { <span class="card-title">Einstellungen zu öffentlichen Account-Daten geändert</span> % } @@ -46,34 +49,38 @@ % my $use_history = users->use_history(uid => $acc->{id}); <div class="row"> <div class="col s12"> - <h2>Account</h2> + <h2><%= L('account.account') %></h2> <table class="striped"> <tr> - <th scope="row">Name</th> + <th scope="row"><%= L('account.name') %></th> <td><a href="/account/name"><i class="material-icons">edit</i></a><%= $acc->{name} %></td> </tr> <tr> - <th scope="row">Mail</th> + <th scope="row"><%= L('account.mail') %></th> <td><a href="/account/mail"><i class="material-icons">edit</i></a><%= $acc->{email} %></td> </tr> <tr> - <th scope="row">Passwort</th> + <th scope="row"><%= L('account.password') %></th> <td><a href="/account/password"><i class="material-icons">edit</i></a></td> </tr> <tr> - <th scope="row">Verbindungen</th> + <th scope="row"><%= L('account.language') %></th> + <td><a href="/account/language"><i class="material-icons">edit</i></a><%= $acc->{languages}[0] // q{} %></td> + </tr> + <tr> + <th scope="row"><%= L('account.connections') %></th> <td> <a href="/account/insight"><i class="material-icons">edit</i></a> % if ($use_history & 0x03) { - Vorschläge aktiv + %= L('account.connections.enabled') % } % else { - <span style="color: #999999;">Vorschläge deaktiviert</span> + <span style="color: #999999;"><%= L('account.connections.disabled') %></span> % } </td> </tr> <tr> - <th scope="row">Sichtbarkeit</th> + <th scope="row"><%= L('account.visibility') %></th> <td> <a href="/account/privacy"><i class="material-icons">edit</i></a> <i class="material-icons">check</i><i class="material-icons"><%= visibility_icon($acc->{default_visibility_str}) %></i> @@ -81,23 +88,23 @@ </td> </tr> <tr> - <th scope="row">Interaktion</th> + <th scope="row"><%= L('account.interaction') %></th> <td> <a href="/account/social"><i class="material-icons">edit</i></a> % if ($acc->{accept_follows}) { - <span>Accounts können dir direkt folgen</span> + <span><%= L('account.interaction.accept-follows') %></span> % } % elsif ($acc->{accept_follow_requests}) { - <span>Accounts können dir auf Anfrage folgen + <span><%= L('account.interaction.accept-follow-requests') %> % if ($num_rx_follow_requests == 1) { - – <a href="/account/social/follow-requests-received"><strong>eine</strong> offene Anfrage</a> + – <a href="/account/social/follow-requests-received"><strong><%= L('account.interaction.one') %></strong> <%= L('account.interaction.open-request') %></a> % } elsif ($num_rx_follow_requests) { - – <a href="/account/social/follow-requests-received"><strong><%= $num_rx_follow_requests %></strong> offene Anfragen</a> + – <a href="/account/social/follow-requests-received"><strong><%= $num_rx_follow_requests %></strong> <%= L('account.interaction.open-requests') %></a> % } </span> % } % else { - <span style="color: #999999;">Accounts können dir nicht folgen</span> + <span style="color: #999999;"><%= L('account.interaction.disabled') %></span> % } </td> </tr> @@ -163,7 +170,7 @@ %= form_for 'logout' => begin %= csrf_field <button class="btn waves-effect waves-light" type="submit" name="action" value="logout"> - Abmelden + %= L('button.logout') </button> %= end </div> diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep index 5ca0e9e..9c2ccde 100644 --- a/templates/landingpage.html.ep +++ b/templates/landingpage.html.ep @@ -144,9 +144,9 @@ </div> <div class="col s10 m10 l6 center-align"> % if (not app->config->{registration}{disabled}) { - <a href="/register" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">add</i>Registrieren</a> + <a href="/register" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">add</i><%= L('button.register') %></a> % } - <a href="/login" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">account_circle</i>Anmelden</a> + <a href="/login" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">account_circle</i><%= L('button.login') %></a> </div> <div class="col s1 m1 l3"> </div> diff --git a/templates/language.html.ep b/templates/language.html.ep new file mode 100644 index 0000000..75df054 --- /dev/null +++ b/templates/language.html.ep @@ -0,0 +1,36 @@ +<h1>Sprache</h1> +%= form_for '/account/language' => (method => 'POST') => begin + %= csrf_field + <div class="row"> + <div class="input-field col s12"> + <div> + <label> + %= radio_button language => 'de-DE' + <span>de-DE: Deutsch (hochdeutsch)</span> + </label> + </div> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + <div> + <label> + %= radio_button language => 'en-GB' + <span>en-GB: English (Great Britain)</span> + </label> + </div> + </div> + </div> + <div class="row"> + <div class="col s3 m3 l3"> + </div> + <div class="col s6 m6 l6 center-align"> + <button class="btn waves-effect waves-light" type="submit" name="action" value="save"> + Speichern + <i class="material-icons right">send</i> + </button> + </div> + <div class="col s3 m3 l3"> + </div> + </div> +%= end diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep index 2279789..ec3b5e0 100644 --- a/templates/layouts/default.html.ep +++ b/templates/layouts/default.html.ep @@ -131,21 +131,21 @@ <div class="col s12 center-align grey-text"> <a href="/about">travelynx</a> v<%= $version // '???' %> <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span> - <a href="/impressum">Impressum</a> + <a href="/impressum"><%= L('footer.imprint') %></a> <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span> - <a href="/impressum">Datenschutz</a> + <a href="/impressum"><%= L('footer.privacy') %></a> <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span> - <a href="/legend">Legende</a> + <a href="/legend"><%= L('footer.legend') %></a> </div> </div> <div class="row"> <div class="col s12 center-align grey-text config"> - Farbschema: - <a onClick="javascript:setTheme('light')">hell</a> + <%= L('footer.colour-scheme') %>: + <a onClick="javascript:setTheme('light')"><%= L('footer.colour-scheme.light') %></a> · - <a onClick="javascript:setTheme('dark')">dunkel</a> + <a onClick="javascript:setTheme('dark')"><%= L('footer.colour-scheme.dark') %></a> · - <a onClick="javascript:setTheme('default')">automatisch</a> + <a onClick="javascript:setTheme('default')"><%= L('footer.colour-scheme.auto') %></a> </div> </div> </div> diff --git a/templates/login.html.ep b/templates/login.html.ep index 3a9cc1f..21f14d3 100644 --- a/templates/login.html.ep +++ b/templates/login.html.ep @@ -75,7 +75,9 @@ </div> <div class="row"> <div class="col s12 m12 l12"> - Mit der Anmeldung stimmst du den <a href="/tos">Nutzungsbedingungen</a> zu. + %= L('login.accept-tos-pre') + <a href="/tos"><%= L('login.tos') %></a> + %= L('login.accept-tos-post') </div> </div> <div class="row"> @@ -83,7 +85,7 @@ </div> <div class="col s6 m6 l6 center-align"> <button class="btn waves-effect waves-light" type="submit" name="action" value="login"> - Anmelden + %= L('button.login') <i class="material-icons right">send</i> </button> </div> @@ -95,7 +97,7 @@ </div> <div class="col s6 m6 l6 center-align"> <a href="/recover"> - Passwort vergessen + %= L('login.forgot-password') </a> </div> <div class="col s3 m3 l3"> @@ -104,7 +106,7 @@ % if (app->config->{registration}{disabled}) { <div class="row" style="margin-top: 2em;"> <div class="col s12 center-align"> - <em>Diese Instanz erlaubt derzeit keine Registrierung neuer Accounts</em> + <em><%= L('login.registration-disabled') %></em> </div> </div> % } diff --git a/templates/register.html.ep b/templates/register.html.ep index f9a486a..e7064da 100644 --- a/templates/register.html.ep +++ b/templates/register.html.ep @@ -8,27 +8,29 @@ <div class="input-field col l6 m12 s12"> <i class="material-icons prefix">account_circle</i> %= text_field 'user', id => 'account', class => 'validate', required => undef, pattern => '[0-9a-zA-Z_-]+', maxlength => 60, autocomplete => 'username' - <label for="account">Name (alphanumerisch)</label> + <label for="account"><%= L('register.name') %></label> </div> <div class="input-field col l6 m12 s12"> <i class="material-icons prefix">email</i> %= email_field 'email', id => 'email', class => 'validate', required => undef, maxlength => 250 - <label for="email">Mail-Adresse</label> + <label for="email"><%= L('register.mail') %></label> </div> <div class="input-field col l6 m12 s12"> <i class="material-icons prefix">lock</i> %= password_field 'password', id => 'password', class => 'validate', required => undef, minlength => 8, maxlength => 10000, autocomplete => 'new-password' - <label for="password">Passwort</label> + <label for="password"><%= L('register.password') %></label> </div> <div class="input-field col l6 m12 s12"> <i class="material-icons prefix">lock</i> %= password_field 'password2', id => 'password2', class => 'validate', required => undef, minlength => 8, maxlength => 10000, autocomplete => 'new-password' - <label for="password2">Passwort wiederholen</label> + <label for="password2"><%= L('register.repeat-password') %></label> </div> </div> <div class="row"> <div class="col s12 m12 l12"> - Mit deiner Registrierung stimmst du den <a href="/tos">Nutzungsbedingungen</a> zu. + %= L('register.accept-tos-pre') + <a href="/tos"><%= L('register.tos') %></a> + %= L('register.accept-tos-post') </div> </div> <div class="row"> @@ -36,7 +38,7 @@ </div> <div class="col s6 m6 l6 center-align"> <button class="btn waves-effect waves-light" type="submit" name="action" value="register"> - Registrieren + %= L('button.register') <i class="material-icons right">send</i> </button> </div> @@ -47,9 +49,7 @@ <div class="row"> <div class="col s12"> <p> - Nach der Registrierung wird ein für 48 Stunden gültiger - Bestätigungslink an die angegebene Mail-Adresse geschickt. Eine - Anmeldung ist erst nach Bestätigung der Mail-Adresse möglich. + %= L('register.expect-confirmation-link') </p> <p> Die Mail-Adresse wird ausschließlich zur Bestätigung der Anmeldung, |