Extended translations system for arrays & extension

Extended the base Laravel translation system to
allow a locale to be based upon another.

Also adds functionality to take base & fallback locales into account when fetching
an array of translations.

Related to work done in #1159
This commit is contained in:
Dan Brown 2018-12-12 20:46:27 +00:00
parent 0e3d507ec2
commit 323bff7d6d
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
16 changed files with 185 additions and 61 deletions

View File

@ -79,6 +79,7 @@ class HomeController extends Controller
{ {
$locale = app()->getLocale(); $locale = app()->getLocale();
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale; $cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
if (cache()->has($cacheKey) && config('app.env') !== 'development') { if (cache()->has($cacheKey) && config('app.env') !== 'development') {
$resp = cache($cacheKey); $resp = cache($cacheKey);
} else { } else {
@ -89,15 +90,6 @@ class HomeController extends Controller
'entities' => trans('entities'), 'entities' => trans('entities'),
'errors' => trans('errors') 'errors' => trans('errors')
]; ];
if ($locale !== 'en') {
$enTrans = [
'common' => trans('common', [], 'en'),
'components' => trans('components', [], 'en'),
'entities' => trans('entities', [], 'en'),
'errors' => trans('errors', [], 'en')
];
$translations = array_replace_recursive($enTrans, $translations);
}
$resp = 'window.translations = ' . json_encode($translations); $resp = 'window.translations = ' . json_encode($translations);
cache()->put($cacheKey, $resp, 120); cache()->put($cacheKey, $resp, 120);
} }

View File

@ -0,0 +1,32 @@
<?php namespace BookStack\Providers;
use BookStack\Translation\Translator;
class TranslationServiceProvider extends \Illuminate\Translation\TranslationServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerLoader();
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];
// When registering the translator component, we'll need to set the default
// locale as well as the fallback locale. So, we'll grab the application
// configuration so we can easily get both of these values from there.
$locale = $app['config']['app.locale'];
$trans = new Translator($loader, $locale);
$trans->setFallback($app['config']['app.fallback_locale']);
return $trans;
});
}
}

View File

@ -0,0 +1,74 @@
<?php namespace BookStack\Translation;
class Translator extends \Illuminate\Translation\Translator
{
/**
* Mapping of locales to their base locales
* @var array
*/
protected $baseLocaleMap = [
'de_informal' => 'de',
];
/**
* Get the translation for a given key.
*
* @param string $key
* @param array $replace
* @param string $locale
* @return string|array|null
*/
public function trans($key, array $replace = [], $locale = null)
{
$translation = $this->get($key, $replace, $locale);
if (is_array($translation)) {
$translation = $this->mergeBackupTranslations($translation, $key, $locale);
}
return $translation;
}
/**
* Merge the fallback translations, and base translations if existing,
* into the provided core key => value array of translations content.
* @param array $translationArray
* @param string $key
* @param null $locale
* @return array
*/
protected function mergeBackupTranslations(array $translationArray, string $key, $locale = null)
{
$fallback = $this->get($key, [], $this->fallback);
$baseLocale = $this->getBaseLocale($locale ?? $this->locale);
$baseTranslations = $baseLocale ? $this->get($key, [], $baseLocale) : [];
return array_replace_recursive($fallback, $baseTranslations, $translationArray);
}
/**
* Get the array of locales to be checked.
*
* @param string|null $locale
* @return array
*/
protected function localeArray($locale)
{
$primaryLocale = $locale ?: $this->locale;
return array_filter([$primaryLocale, $this->getBaseLocale($primaryLocale), $this->fallback]);
}
/**
* Get the locale to extend for the given locale.
*
* @param string $locale
* @return string|null
*/
protected function getBaseLocale($locale)
{
return $this->baseLocaleMap[$locale] ?? null;
}
}

View File

@ -6,6 +6,7 @@
"type": "project", "type": "project",
"require": { "require": {
"php": ">=7.0.0", "php": ">=7.0.0",
"ext-json": "*",
"ext-tidy": "*", "ext-tidy": "*",
"ext-dom": "*", "ext-dom": "*",
"laravel/framework": "~5.5.44", "laravel/framework": "~5.5.44",

View File

@ -187,7 +187,6 @@ return [
Illuminate\Redis\RedisServiceProvider::class, Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class, Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class, Illuminate\View\ViewServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class, Illuminate\Notifications\NotificationServiceProvider::class,
@ -205,6 +204,7 @@ return [
* Application Service Providers... * Application Service Providers...
*/ */
BookStack\Providers\PaginationServiceProvider::class, BookStack\Providers\PaginationServiceProvider::class,
BookStack\Providers\TranslationServiceProvider::class,
BookStack\Providers\AuthServiceProvider::class, BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\AppServiceProvider::class, BookStack\Providers\AppServiceProvider::class,

View File

@ -1,8 +1,6 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [
// Extends 'de'
return [
//
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -1,7 +1,8 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [ // Extends 'de'
return [
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Authentication Language Lines | Authentication Language Lines
@ -13,6 +14,7 @@ $de_informal = [
| |
*/ */
'throttle' => 'Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.', 'throttle' => 'Zu viele Anmeldeversuche. Bitte versuche es in :seconds Sekunden erneut.',
/** /**
* Login & Register * Login & Register
*/ */
@ -20,6 +22,7 @@ $de_informal = [
'register_confirm' => 'Bitte prüfe Deinen Posteingang und bestätig die Registrierung.', 'register_confirm' => 'Bitte prüfe Deinen Posteingang und bestätig die Registrierung.',
'registration_email_domain_invalid' => 'Du kannst dich mit dieser E-Mail nicht registrieren.', 'registration_email_domain_invalid' => 'Du kannst dich mit dieser E-Mail nicht registrieren.',
'register_success' => 'Vielen Dank für Deine Registrierung! Die Daten sind gespeichert und Du bist angemeldet.', 'register_success' => 'Vielen Dank für Deine Registrierung! Die Daten sind gespeichert und Du bist angemeldet.',
/** /**
* Password Reset * Password Reset
*/ */
@ -28,6 +31,7 @@ $de_informal = [
'reset_password_success' => 'Dein Passwort wurde erfolgreich zurückgesetzt.', 'reset_password_success' => 'Dein Passwort wurde erfolgreich zurückgesetzt.',
'email_reset_text' => 'Du erhältsts diese E-Mail, weil jemand versucht hat, Dein Passwort zurückzusetzen.', 'email_reset_text' => 'Du erhältsts diese E-Mail, weil jemand versucht hat, Dein Passwort zurückzusetzen.',
'email_reset_not_requested' => 'Wenn Du das nicht warst, brauchst Du nichts weiter zu tun.', 'email_reset_not_requested' => 'Wenn Du das nicht warst, brauchst Du nichts weiter zu tun.',
/** /**
* Email Confirmation * Email Confirmation
*/ */
@ -41,5 +45,3 @@ $de_informal = [
'email_not_confirmed_click_link' => 'Bitte klicke auf den Link in der E-Mail, die Du nach der Registrierung erhalten hast.', 'email_not_confirmed_click_link' => 'Bitte klicke auf den Link in der E-Mail, die Du nach der Registrierung erhalten hast.',
'email_not_confirmed_resend' => 'Wenn Du die E-Mail nicht erhalten hast, kannst Du die Nachricht erneut anfordern. Fülle hierzu bitte das folgende Formular aus:', 'email_not_confirmed_resend' => 'Wenn Du die E-Mail nicht erhalten hast, kannst Du die Nachricht erneut anfordern. Fülle hierzu bitte das folgende Formular aus:',
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -1,11 +1,9 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [ // Extends 'de'
return [
/** /**
* Email Content * Email Content
*/ */
'email_action_help' => 'Sollte es beim Anklicken der Schaltfläche ":action_text" Probleme geben, öffne die folgende URL in Deinem Browser:', 'email_action_help' => 'Sollte es beim Anklicken der Schaltfläche ":action_text" Probleme geben, öffne die folgende URL in Deinem Browser:',
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -1,12 +1,10 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [ // Extends 'de'
return [
/** /**
* Image Manager * Image Manager
*/ */
'image_delete_confirm' => 'Bitte klicke erneut auf löschen, wenn Du dieses Bild wirklich entfernen möchtest.', 'image_delete_confirm' => 'Bitte klicke erneut auf löschen, wenn Du dieses Bild wirklich entfernen möchtest.',
'image_dropzone' => 'Ziehe Bilder hierher oder klicke hier, um ein Bild auszuwählen', 'image_dropzone' => 'Ziehe Bilder hierher oder klicke hier, um ein Bild auszuwählen',
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -1,21 +1,24 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [ // Extends 'de'
return [
/** /**
* Shared * Shared
*/ */
'no_pages_viewed' => 'Du hast bisher keine Seiten angesehen.', 'no_pages_viewed' => 'Du hast bisher keine Seiten angesehen.',
'no_pages_recently_created' => 'Du hast bisher keine Seiten angelegt.', 'no_pages_recently_created' => 'Du hast bisher keine Seiten angelegt.',
'no_pages_recently_updated' => 'Du hast bisher keine Seiten aktualisiert.', 'no_pages_recently_updated' => 'Du hast bisher keine Seiten aktualisiert.',
/** /**
* Books * Books
*/ */
'books_delete_confirmation' => 'Bist Du sicher, dass Du dieses Buch löschen möchtest?', 'books_delete_confirmation' => 'Bist Du sicher, dass Du dieses Buch löschen möchtest?',
/** /**
* Chapters * Chapters
*/ */
'chapters_delete_confirm' => 'Bist Du sicher, dass Du dieses Kapitel löschen möchtest?', 'chapters_delete_confirm' => 'Bist Du sicher, dass Du dieses Kapitel löschen möchtest?',
/** /**
* Pages * Pages
*/ */
@ -30,6 +33,7 @@ $de_informal = [
'time_b' => 'in den letzten :minCount Minuten', 'time_b' => 'in den letzten :minCount Minuten',
'message' => ':start :time. Achte darauf, keine Änderungen von anderen Benutzern zu überschreiben!', 'message' => ':start :time. Achte darauf, keine Änderungen von anderen Benutzern zu überschreiben!',
], ],
/** /**
* Editor sidebar * Editor sidebar
*/ */
@ -39,15 +43,15 @@ $de_informal = [
'attachments_dropzone' => 'Ziehe Dateien hierher oder klicke hier, um eine Datei auszuwählen', 'attachments_dropzone' => 'Ziehe Dateien hierher oder klicke hier, um eine Datei auszuwählen',
'attachments_explain_link' => 'Wenn Du keine Datei hochladen möchtest, kannst Du stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet verweisen.', 'attachments_explain_link' => 'Wenn Du keine Datei hochladen möchtest, kannst Du stattdessen einen Link hinzufügen. Dieser Link kann auf eine andere Seite oder eine Datei im Internet verweisen.',
'attachments_edit_drop_upload' => 'Ziehe Dateien hierher, um diese hochzuladen und zu überschreiben', 'attachments_edit_drop_upload' => 'Ziehe Dateien hierher, um diese hochzuladen und zu überschreiben',
/** /**
* Comments * Comments
*/ */
'comment_placeholder' => 'Gib hier Deine Kommentare ein (Markdown unterstützt)', 'comment_placeholder' => 'Gib hier Deine Kommentare ein (Markdown unterstützt)',
'comment_delete_confirm' => 'Möchtst Du diesen Kommentar wirklich löschen?', 'comment_delete_confirm' => 'Möchtst Du diesen Kommentar wirklich löschen?',
/** /**
* Revision * Revision
*/ */
'revision_delete_confirm' => 'Bist Du sicher, dass Du diese Revision löschen möchtest?', 'revision_delete_confirm' => 'Bist Du sicher, dass Du diese Revision löschen möchtest?',
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -1,10 +1,11 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [ // Extends 'de'
return [
// Pages // Pages
'permission' => 'Du hast keine Berechtigung, auf diese Seite zuzugreifen.', 'permission' => 'Du hast keine Berechtigung, auf diese Seite zuzugreifen.',
'permissionJson' => 'Du hast keine Berechtigung, die angeforderte Aktion auszuführen.', 'permissionJson' => 'Du hast keine Berechtigung, die angeforderte Aktion auszuführen.',
// Auth // Auth
'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melde dich an.', 'email_already_confirmed' => 'Die E-Mail-Adresse ist bereits bestätigt. Bitte melde dich an.',
'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registriere dich erneut.', 'email_confirmation_invalid' => 'Der Bestätigungslink ist nicht gültig oder wurde bereits verwendet. Bitte registriere dich erneut.',
@ -12,18 +13,20 @@ $de_informal = [
'social_account_email_in_use' => 'Die E-Mail-Adresse ":email" ist bereits registriert. Wenn Du bereits registriert bist, kannst Du Dein :socialAccount-Konto in Deinen Profil-Einstellungen verknüpfen.', 'social_account_email_in_use' => 'Die E-Mail-Adresse ":email" ist bereits registriert. Wenn Du bereits registriert bist, kannst Du Dein :socialAccount-Konto in Deinen Profil-Einstellungen verknüpfen.',
'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Du kannst das in Deinen Profil-Einstellungen tun.', 'social_account_not_used' => 'Dieses :socialAccount-Konto ist bisher keinem Benutzer zugeordnet. Du kannst das in Deinen Profil-Einstellungen tun.',
'social_account_register_instructions' => 'Wenn Du bisher kein Social-Media Konto besitzt, kannst Du ein solches Konto mit der :socialAccount Option anlegen.', 'social_account_register_instructions' => 'Wenn Du bisher kein Social-Media Konto besitzt, kannst Du ein solches Konto mit der :socialAccount Option anlegen.',
// System // System
'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stelle sicher, dass dieser Ordner auf dem Server beschreibbar ist.', 'path_not_writable' => 'Die Datei kann nicht in den angegebenen Pfad :filePath hochgeladen werden. Stelle sicher, dass dieser Ordner auf dem Server beschreibbar ist.',
'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfe, ob die GD PHP-Erweiterung installiert ist.', 'cannot_create_thumbs' => 'Der Server kann keine Vorschau-Bilder erzeugen. Bitte prüfe, ob die GD PHP-Erweiterung installiert ist.',
'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuche es mit einer kleineren Datei.', 'server_upload_limit' => 'Der Server verbietet das Hochladen von Dateien mit dieser Dateigröße. Bitte versuche es mit einer kleineren Datei.',
// Pages // Pages
'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stelle sicher, dass Du mit dem Internet verbunden bist, bevor Du den Entwurf dieser Seite speicherst.', 'page_draft_autosave_fail' => 'Fehler beim Speichern des Entwurfs. Stelle sicher, dass Du mit dem Internet verbunden bist, bevor Du den Entwurf dieser Seite speicherst.',
'page_custom_home_deletion' => 'Eine als Startseite gesetzte Seite kann nicht gelöscht werden.', 'page_custom_home_deletion' => 'Eine als Startseite gesetzte Seite kann nicht gelöscht werden.',
// Users // Users
'users_cannot_delete_only_admin' => 'Du kannst den einzigen Administrator nicht löschen.', 'users_cannot_delete_only_admin' => 'Du kannst den einzigen Administrator nicht löschen.',
'users_cannot_delete_guest' => 'Du kannst den Gast-Benutzer nicht löschen', 'users_cannot_delete_guest' => 'Du kannst den Gast-Benutzer nicht löschen',
// Error pages // Error pages
'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Du angefordert hast, wurde nicht gefunden.', 'sorry_page_not_found' => 'Entschuldigung. Die Seite, die Du angefordert hast, wurde nicht gefunden.',
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -1,8 +1,6 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [
// Extends 'de'
return [
//
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -1,8 +1,6 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [
// Extends 'de'
return [
//
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -1,12 +1,13 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [ // Extends 'de'
return [
/** /**
* Settings text strings * Settings text strings
* Contains all text strings used in the general settings sections of BookStack * Contains all text strings used in the general settings sections of BookStack
* including users and roles. * including users and roles.
*/ */
/** /**
* App settings * App settings
*/ */
@ -14,17 +15,20 @@ $de_informal = [
'app_primary_color_desc' => "Dies sollte ein HEX Wert sein.\nWenn Du nichts eingibst, wird die Anwendung auf die Standardfarbe zurückgesetzt.", 'app_primary_color_desc' => "Dies sollte ein HEX Wert sein.\nWenn Du nichts eingibst, wird die Anwendung auf die Standardfarbe zurückgesetzt.",
'app_homepage_desc' => 'Wähle eine Seite als Startseite aus, die statt der Standardansicht angezeigt werden soll. Seitenberechtigungen werden für die ausgewählten Seiten ignoriert.', 'app_homepage_desc' => 'Wähle eine Seite als Startseite aus, die statt der Standardansicht angezeigt werden soll. Seitenberechtigungen werden für die ausgewählten Seiten ignoriert.',
'app_homepage_books' => 'Oder wähle die Buch-Übersicht als Startseite. Das wird die Seiten-Auswahl überschreiben.', 'app_homepage_books' => 'Oder wähle die Buch-Übersicht als Startseite. Das wird die Seiten-Auswahl überschreiben.',
/** /**
* Maintenance settings * Maintenance settings
*/ */
'maint_image_cleanup_desc' => 'Überprüft Seiten- und Versionsinhalte auf ungenutzte und mehrfach vorhandene Bilder. Erstelle vor dem Start ein Backup Deiner Datenbank und Bilder.', 'maint_image_cleanup_desc' => 'Überprüft Seiten- und Versionsinhalte auf ungenutzte und mehrfach vorhandene Bilder. Erstelle vor dem Start ein Backup Deiner Datenbank und Bilder.',
'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchtest Du diese Bilder löschen?', 'maint_image_cleanup_warning' => ':count eventuell unbenutze Bilder wurden gefunden. Möchtest Du diese Bilder löschen?',
/** /**
* Role settings * Role settings
*/ */
'role_delete_confirm' => 'Du möchtest die Rolle ":roleName" löschen.', 'role_delete_confirm' => 'Du möchtest die Rolle ":roleName" löschen.',
'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Du kannst unten eine neue Rolle auswählen, die Du diesen Benutzern zuordnen möchtest.', 'role_delete_users_assigned' => 'Diese Rolle ist :userCount Benutzern zugeordnet. Du kannst unten eine neue Rolle auswählen, die Du diesen Benutzern zuordnen möchtest.',
'role_delete_sure' => 'Bist Du sicher, dass Du diese Rolle löschen möchtest?', 'role_delete_sure' => 'Bist Du sicher, dass Du diese Rolle löschen möchtest?',
/** /**
* Users * Users
*/ */
@ -32,5 +36,3 @@ $de_informal = [
'users_delete_confirm' => 'Bist Du sicher, dass Du diesen Benutzer löschen möchtest?', 'users_delete_confirm' => 'Bist Du sicher, dass Du diesen Benutzer löschen möchtest?',
'users_social_accounts_info' => 'Hier kannst Du andere Social-Media-Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Du ein Social-Media Konto löschst, bleibt der Zugriff erhalten. Entferne in diesem Falle die Berechtigung in Deinen Profil-Einstellungen des verknüpften Social-Media-Kontos.', 'users_social_accounts_info' => 'Hier kannst Du andere Social-Media-Konten für eine schnellere und einfachere Anmeldung verknüpfen. Wenn Du ein Social-Media Konto löschst, bleibt der Zugriff erhalten. Entferne in diesem Falle die Berechtigung in Deinen Profil-Einstellungen des verknüpften Social-Media-Kontos.',
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -1,8 +1,6 @@
<?php <?php
$de_formal = (include resource_path() . '/lang/de/' . basename(__FILE__));
$de_informal = [
// Extends 'de'
return [
//
]; ];
return array_replace($de_formal, $de_informal);

View File

@ -81,4 +81,30 @@ class LanguageTest extends TestCase
$this->assertTrue(config('app.rtl'), "App RTL config should have been set to true by middleware"); $this->assertTrue(config('app.rtl'), "App RTL config should have been set to true by middleware");
} }
public function test_de_informal_falls_base_to_de()
{
// Base de back value
$deBack = trans()->get('common.cancel', [], 'de', false);
$this->assertEquals('Abbrechen', $deBack);
// Ensure de_informal has no value set
$this->assertEquals('common.cancel', trans()->get('common.cancel', [], 'de_informal', false));
// Ensure standard trans falls back to de
$this->assertEquals($deBack, trans('common.cancel', [], 'de_informal'));
// Ensure de_informal gets its own values where set
$deEmailActionHelp = trans()->get('common.email_action_help', [], 'de', false);
$enEmailActionHelp = trans()->get('common.email_action_help', [], 'en', false);
$deInformalEmailActionHelp = trans()->get('common.email_action_help', [], 'de_informal', false);
$this->assertNotEquals($deEmailActionHelp, $deInformalEmailActionHelp);
$this->assertNotEquals($enEmailActionHelp, $deInformalEmailActionHelp);
}
public function test_de_informal_falls_base_to_de_in_js_endpoint()
{
$this->asEditor();
setting()->putUser($this->getEditor(), 'language', 'de_informal');
$transResp = $this->get('/translations');
$transResp->assertSee('"cancel":"Abbrechen"');
}
} }