Switch the app to German as its only language

The app is used internally only and its settings page was German
already. Mails, controller error messages (shown in the form), the
settings section name and the mail sender are now written in German
directly instead of going through IL10N without any translation files.
Nextcloud's own mail footer is requested in German as well.
This commit is contained in:
Patrick Niebeling committed 2026-09-30 10:39:12 +02:00
1 parent 578b235f08
commit 1ffc0765ea
6 files changed
+77 -66

No files matched your search

+11 -11
View File
@@ -34,7 +34,7 @@ class WorkflowController extends OCSController {
private function currentUserId(): string {
$user = $this->userSession->getUser();
if ($user === null) {
throw new OCSPreconditionFailedException('Not logged in');
throw new OCSPreconditionFailedException('Nicht angemeldet');
}
return $user->getUID();
}
@@ -114,12 +114,12 @@ class WorkflowController extends OCSController {
try {
$workflow = $this->workflowMapper->findById($id);
} catch (DoesNotExistException $e) {
throw new OCSNotFoundException('Workflow not found');
throw new OCSNotFoundException('Workflow nicht gefunden');
}
if (!in_array($workflow->getBoardId(), $this->ownedBoardIds(), true)) {
// Same answer as for a missing id: don't reveal other users' workflows.
throw new OCSNotFoundException('Workflow not found');
throw new OCSNotFoundException('Workflow nicht gefunden');
}
return $workflow;
}
@@ -133,7 +133,7 @@ class WorkflowController extends OCSController {
try {
return $this->deckService->listOwnedBoards($userId);
} catch (DeckUnavailableException $e) {
throw new OCSPreconditionFailedException('Could not load your Deck boards: ' . $e->getMessage());
throw new OCSPreconditionFailedException('Deine Deck-Boards konnten nicht geladen werden: ' . $e->getMessage());
}
}
@@ -147,24 +147,24 @@ class WorkflowController extends OCSController {
private function assertDeck(): void {
$user = $this->userSession->getUser();
if ($user === null) {
throw new OCSPreconditionFailedException('Not logged in');
throw new OCSPreconditionFailedException('Nicht angemeldet');
}
try {
$this->deckService->assertDeckAvailable($user);
} catch (DeckUnavailableException $e) {
throw new OCSPreconditionFailedException('Deck is not available: ' . $e->getMessage());
throw new OCSPreconditionFailedException('Deck ist nicht verfügbar: ' . $e->getMessage());
}
}
private function validate(string $title, int $boardId, int $sourceStackId, int $targetStackId, string $notifyMode): void {
if (trim($title) === '') {
throw new OCSBadRequestException('Title must not be empty');
throw new OCSBadRequestException('Bitte einen Titel angeben.');
}
if ($sourceStackId === $targetStackId) {
throw new OCSBadRequestException('Source and target stack must differ');
throw new OCSBadRequestException('Quell- und Ziel-Stapel müssen sich unterscheiden.');
}
if (!NotificationPolicy::isValidMode($notifyMode)) {
throw new OCSBadRequestException('Notification mode must be one of: ' . implode(', ', NotificationPolicy::MODES));
throw new OCSBadRequestException('Ungültige E-Mail-Einstellung, erlaubt sind: ' . implode(', ', NotificationPolicy::MODES));
}
// Reject references to boards or stacks that are gone (or were never
@@ -184,7 +184,7 @@ class WorkflowController extends OCSController {
}
}
if ($board === null || $board['archived']) {
throw new OCSBadRequestException('The selected board does not exist, is archived or is not owned by you');
throw new OCSBadRequestException('Das gewählte Board existiert nicht, ist archiviert oder gehört dir nicht.');
}
$stackIds = array_map(
@@ -192,7 +192,7 @@ class WorkflowController extends OCSController {
$this->deckService->listStacks($boardId),
);
if (!in_array($sourceStackId, $stackIds, true) || !in_array($targetStackId, $stackIds, true)) {
throw new OCSBadRequestException('The selected stack does not exist on this board');
throw new OCSBadRequestException('Der gewählte Stapel existiert auf diesem Board nicht.');
}
}
+54 -48
View File
@@ -6,19 +6,31 @@ namespace OCA\WorkflowDeckAutomation\Service;
use OCA\Deck\Db\Card;
use OCA\WorkflowDeckAutomation\Db\Workflow;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\L10N\IFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Throwable;
/**
* The app is used internally only and speaks German, like its settings
* page: texts are written in German directly instead of going through
* IL10N with English source strings and a translation file.
*/
class NotificationMailer {
private const APP_ID = 'workflow_deck_automation';
private const SENDER_NAME = 'Deck Workflow-Automatisierung';
/**
* Language of Nextcloud's own footer text ("automatically sent email,
* please do not reply"), which would otherwise follow the instance
* default — in cron there is no request to take a language from.
*/
private const FOOTER_LANGUAGE = 'de';
/**
* Descriptions are unbounded in Deck; keep the mail readable and well
* clear of any MTA size limits.
@@ -28,7 +40,6 @@ class NotificationMailer {
public function __construct(
private IMailer $mailer,
private IURLGenerator $urlGenerator,
private IFactory $l10nFactory,
private LoggerInterface $logger,
) {
}
@@ -50,7 +61,6 @@ class NotificationMailer {
}
$count = count($entries);
$l10n = $this->l10nFor($recipient);
try {
$template = $this->mailer->createEMailTemplate(self::APP_ID . '.CardsMoved', [
@@ -58,20 +68,22 @@ class NotificationMailer {
'cardTitles' => array_map(static fn (array $entry) => $entry['card']->getTitle(), $entries),
]);
$template->setSubject($count === 1
? $l10n->t('Deck card moved: %s', [$entries[0]['card']->getTitle()])
: $l10n->n('%n Deck card moved', '%n Deck cards moved', $count));
? sprintf('Deck-Karte verschoben: %s', $entries[0]['card']->getTitle())
: sprintf('%d Deck-Karten verschoben', $count));
$template->addHeader();
$template->addHeading($l10n->n('A card was moved automatically', '%n cards were moved automatically', $count), false);
$template->addHeading($count === 1
? 'Eine Karte wurde automatisch verschoben'
: sprintf('%d Karten wurden automatisch verschoben', $count), false);
foreach ($entries as $entry) {
$this->addCard($template, $l10n, $entry['workflow'], $entry['card'], $entry['targets']);
$this->addCard($template, $entry['workflow'], $entry['card'], $entry['targets']);
}
$template->addFooter();
$template->addFooter('', self::FOOTER_LANGUAGE);
$message = $this->mailer->createMessage();
$message->setTo([$email => $recipient->getDisplayName()]);
$message->setFrom([Util::getDefaultEmailAddress('noreply') => $l10n->t('Deck Workflow Automation')]);
$message->setFrom([Util::getDefaultEmailAddress('noreply') => self::SENDER_NAME]);
$message->useTemplate($template);
$failedRecipients = $this->mailer->send($message);
@@ -91,21 +103,24 @@ class NotificationMailer {
/**
* @param array{board: string, sourceStack: string, targetStack: string} $targets
*/
private function addCard(IEMailTemplate $template, IL10N $l10n, Workflow $workflow, Card $card, array $targets): void {
$template->addBodyText($l10n->t(
'The card "%1$s" was moved because it is overdue (workflow "%2$s").',
[$card->getTitle(), $workflow->getTitle()],
));
private function addCard(IEMailTemplate $template, Workflow $workflow, Card $card, array $targets): void {
// Single-argument addBodyText() runs htmlspecialchars() itself, so
// these board- and stack-titles (user input) are safe as-is —
// unlike the description below, which passes both parts.
$template->addBodyText($l10n->t(
'Board "%1$s": moved from "%2$s" to "%3$s".',
[$targets['board'], $targets['sourceStack'], $targets['targetStack']],
// these card-, workflow-, board- and stack-titles (user input) are
// safe as-is — unlike the description below, which passes both parts.
$template->addBodyText(sprintf(
'Die Karte „%1$s“ wurde verschoben, weil sie überfällig ist (Workflow „%2$s“).',
$card->getTitle(),
$workflow->getTitle(),
));
$this->addCardDescription($template, $l10n, $card);
$template->addBodyText(sprintf(
'Board „%1$s“: von „%2$s“ nach „%3$s“ verschoben.',
$targets['board'],
$targets['sourceStack'],
$targets['targetStack'],
));
$this->addCardDescription($template, $card);
$template->addBodyButton(
$l10n->t('Open card'),
'Karte öffnen',
$this->urlGenerator->getAbsoluteURL('/apps/deck/board/' . $workflow->getBoardId() . '/card/' . $card->getId()),
);
}
@@ -125,11 +140,10 @@ class NotificationMailer {
return;
}
$l10n = $this->l10nFor($user);
$reason = match ($brokenTarget) {
WorkflowRunner::TARGET_SOURCE_STACK => $l10n->t('its source stack no longer exists'),
WorkflowRunner::TARGET_TARGET_STACK => $l10n->t('its target stack no longer exists'),
default => $l10n->t('its board no longer exists or is no longer available to you'),
WorkflowRunner::TARGET_SOURCE_STACK => 'sein Quell-Stapel nicht mehr existiert',
WorkflowRunner::TARGET_TARGET_STACK => 'sein Ziel-Stapel nicht mehr existiert',
default => 'sein Board nicht mehr existiert oder für dich nicht mehr verfügbar ist',
};
try {
@@ -138,22 +152,23 @@ class NotificationMailer {
$template = $this->mailer->createEMailTemplate(self::APP_ID . '.WorkflowDisabled', [
'workflowTitle' => $workflow->getTitle(),
]);
$template->setSubject($l10n->t('Deck workflow deactivated: %s', [$workflow->getTitle()]));
$template->setSubject(sprintf('Deck-Workflow deaktiviert: %s', $workflow->getTitle()));
$template->addHeader();
$template->addHeading($l10n->t('A workflow was deactivated'), false);
$template->addBodyText($l10n->t(
'The workflow "%1$s" was switched off automatically because %2$s. No cards are being moved by it any more.',
[$workflow->getTitle(), $reason],
$template->addHeading('Ein Workflow wurde deaktiviert', false);
$template->addBodyText(sprintf(
'Der Workflow „%1$s“ wurde automatisch ausgeschaltet, weil %2$s. Er verschiebt keine Karten mehr.',
$workflow->getTitle(),
$reason,
));
$template->addBodyText($l10n->t(
'Delete the workflow or point it at an existing board and stack to reactivate it. You will not be reminded about this workflow again.',
));
$template->addBodyButton($l10n->t('Open settings'), $settingsLink);
$template->addFooter();
$template->addBodyText(
'Lösche den Workflow oder stelle ein vorhandenes Board und vorhandene Stapel ein, um ihn wieder zu aktivieren. An diesen Workflow wirst du nicht noch einmal erinnert.',
);
$template->addBodyButton('Einstellungen öffnen', $settingsLink);
$template->addFooter('', self::FOOTER_LANGUAGE);
$message = $this->mailer->createMessage();
$message->setTo([$email => $user->getDisplayName()]);
$message->setFrom([Util::getDefaultEmailAddress('noreply') => $l10n->t('Deck Workflow Automation')]);
$message->setFrom([Util::getDefaultEmailAddress('noreply') => self::SENDER_NAME]);
$message->useTemplate($template);
$failedRecipients = $this->mailer->send($message);
@@ -170,15 +185,6 @@ class NotificationMailer {
}
}
/**
* The recipient's own language. Cron has no request to take one from, so
* an injected IL10N would always speak the instance default — wrong as
* soon as a mail goes to someone other than whoever set that default.
*/
private function l10nFor(IUser $user): IL10N {
return $this->l10nFactory->get(self::APP_ID, $this->l10nFactory->getUserLanguage($user));
}
/**
* Appends the card's description, if it has one.
*
@@ -186,7 +192,7 @@ class NotificationMailer {
* rendered: pulling a Markdown parser in just for the mail would be a
* lot of machinery, and unrendered Markdown still reads fine.
*/
private function addCardDescription(IEMailTemplate $template, IL10N $l10n, Card $card): void {
private function addCardDescription(IEMailTemplate $template, Card $card): void {
$description = trim((string)$card->getDescription());
if ($description === '') {
return;
@@ -196,7 +202,7 @@ class NotificationMailer {
$description = mb_substr($description, 0, self::MAX_DESCRIPTION_LENGTH) . ' […]';
}
$template->addBodyText($l10n->t('Card content:'));
$template->addBodyText('Karteninhalt:');
// addBodyText() only runs htmlspecialchars() when it has to build the
// plain-text part itself. Passing both parts means escaping is on us
+2 -3
View File
@@ -5,13 +5,11 @@ declare(strict_types=1);
namespace OCA\WorkflowDeckAutomation\Settings;
use OCA\WorkflowDeckAutomation\AppInfo\Application;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class PersonalSection implements IIconSection {
public function __construct(
private IL10N $l10n,
private IURLGenerator $urlGenerator,
) {
}
@@ -21,7 +19,8 @@ class PersonalSection implements IIconSection {
}
public function getName(): string {
return $this->l10n->t('Deck workflow automation');
// German only, like the rest of this internal app.
return 'Deck Workflow-Automatisierung';
}
public function getPriority(): int {