mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
Merge branch 'pr-110' into review-110
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
245a84facf
15 files changed
+4473
-9
No files matched your search
@@ -29,6 +29,11 @@ tabs, read for parcels, invoices or meetings, or offered as a calendar event, an
|
||||
no longer become contacts. A forged invitation in Spam stays out of the calendar. Read receipts
|
||||
filtered into Spam are still matched to the mail they are about. No migration.
|
||||
|
||||
**plMail speaks French.** Choose **Français** under **Settings → General → Language**, or when
|
||||
creating the first account. The whole interface is translated, including the admin panel, the
|
||||
validation and sign-in messages and the emoji picker's search; French also defaults to the
|
||||
24-hour clock, as German does. No migration.
|
||||
|
||||
## v0.3.5 — 2026-10-13
|
||||
|
||||
**Browsers and password managers kept offering your logins in client-secret fields.** Opening
|
||||
|
||||
@@ -80,7 +80,7 @@ provider doesn't.
|
||||
with what changed since the one they run.
|
||||
- **Yours** — two-factor auth, sign-in through Authentik, Keycloak or any other OpenID Connect
|
||||
provider, per-app JMAP passwords, 41 themes, ten icons for the tab and the phone's home screen,
|
||||
English, German and Pirate.
|
||||
English, German, French and Pirate.
|
||||
|
||||
## A look around
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[{"shortcodes":["visage_rieur"],"annotation":"visage rieur","tags":["content","dents","grand sourire","heureux","joyeux","rire","sourire","super","visage","visage très souriant","émoticône"],"emoji":"😀","order":1,"group":0,"version":1},{"shortcodes":["visage_souriant_avec_de_grands_yeux"],"annotation":"visage souriant avec de grands yeux","tags":["content","dents","formidable","grand sourire","heureux","sourire","super","visage très souriant avec la bouche ouverte","émoticône"],"emoji":"😃","order":2,"group":0,"version":0.6},{"shortcodes":["visage_tres_souriant_aux_yeux_rieurs"],"annotation":"visage très souriant aux yeux rieurs","tags":["content","grand sourire","heureux","joyeux","lol","mdr","rire","sourire","sourire mignon","visage avec la bouche ouverte et les yeux souriants","émoticône"],"emoji":"😄","order":3,"group":0,"version":0.6,"emoticon":":D"},{"shortcodes":["visage_souriant_aux_yeux_rieurs"],"annotation":"visage souriant aux yeux rieurs","tags":["content","dents","grand sourire","heureux","sourire","sourire de toutes ses dents","super","visage","visage souriant avec yeux souriants","yeux souriants","émoticône"],"emoji":"😁","order":4,"group":0,"version":0.6},{"shortcodes":["visage_souriant_avec_yeux_plisses"],"annotation":"visage souriant avec yeux plissés","tags":["ahah","bouche","content","lol","mdr","rire","sourire","visage","visage souriant avec bouche ouverte et yeux fermés","émoticône"],"emoji":"😆","order":5,"group":0,"version":0.6,"emoticon":"XD"},{"shortcodes":["visage_souriant_avec_une_goutte_de_sueur"],"annotation":"visage souriant avec une goutte de sueur","tags":["excité","froid","goutte","sourire","sourire nerveux","stressé","sueur","sueurs froides","émoticône"],"emoji":"😅","order":6,"group":0,"version":0.6},{"shortcodes":["se_rouler_par_terre_de_rire"],"annotation":"se rouler par terre de rire","tags":["content","drôle","hilare","joyeux","larmes","lol","mdr","pleurer de rire","rire","rire aux larmes","se rouler par terre","se tordre de rire","visage","visage qui rit aux larmes","émoticône"],"emoji":"🤣","order":7,"group":0,"version":3,"emoticon":":'D"},{"shortcodes":["visage_riant_aux_larmes"],"annotation":"visage riant aux larmes","tags":["content","heureux","joie","larmes","lol","mdr","pleurer de joie","pleurer de rire","rire aux larmes","sourire","émoticône"],"emoji":"😂","order":8,"group":0,"version":0.6,"emoticon":":')"},{"shortcodes":["visage_avec_un_leger_sourire"],"annotation":"visage avec un léger sourire","tags":["content","heureux","léger sourire","sourire","visage souriant","émoticône"],"emoji":"🙂","order":9,"group":0,"version":1,"emoticon":":)"},{"shortcodes":["tete_a_lenvers"],"annotation":"tête à l’envers","tags":["incroyable","renversée","tête","visage","émoticône"],"emoji":"🙃","order":10,"group":0,"version":1},{"shortcodes":["visage_qui_fond"],"annotation":"visage qui fond","tags":["disparaître","fondre","gêne","liquide","liquéfié","sarcasme","se dissoudre"],"emoji":"🫠","order":11,"group":0,"version":14},{"shortcodes":["visage_faisant_un_clin_doeil"],"annotation":"visage faisant un clin d’œil","tags":["blague","clin d’œil","flirter","plaisanter","sexy","visage","émoticône"],"emoji":"😉","order":12,"group":0,"version":0.6,"emoticon":";)"},{"shortcodes":["visage_souriant_avec_yeux_rieurs"],"annotation":"visage souriant avec yeux rieurs","tags":["content","heureux","rougir","satisfait","sourire","visage","visage qui rougit","visage souriant avec yeux souriants","yeux","émoticône","œil"],"emoji":"😊","order":13,"group":0,"version":0.6,"emoticon":":>"},{"shortcodes":["visage_souriant_avec_aureole"],"annotation":"visage souriant avec auréole","tags":["ange","angélique","auréole","bible","béni","heureux","innocent","paix","sourire","spirituel","émoticône"],"emoji":"😇","order":14,"group":0,"version":1,"emoticon":"O:)"},{"shortcodes":["visage_souriant_avec_coeurs"],"annotation":"visage souriant avec cœurs","tags":["adorer","aimer","amour","béguin","coeur","cœurs","je t’aime","j’adore","smiley avec 3 coeurs","sourire"],"emoji":"🥰","order":15,"group":0,"version":11},{"shortcodes":["visage_souriant_avec_yeux_en_forme_de_coeur"],"annotation":"visage souriant avec yeux en forme de cœur","tags":["amour","amoureux","cœur","romantique","sourire","visage","yeux","émoticône","œil"],"emoji":"😍","order":16,"group":0,"version":0.6},{"shortcodes":["visage_avec_des_etoiles_a_la_place_des_yeux"],"annotation":"visage avec des étoiles à la place des yeux","tags":["grand sourire","heureux","pressé","surexcité","tête avec étoiles","visage","wow","yeux","étoile","étoiles dans les yeux"],"emoji":"🤩","order":17,"group":0,"version":5},{"shortcodes":["visage_envoyant_un_bisou"],"annotation":"visage envoyant un bisou","tags":["amour","baiser","bise","bisou et cœur","bonjour","bonne nuit","cœur","embrasser","je t’aime","visage qui fait une bise avec un cœur","émoticône"],"Line truncated
|
||||
@@ -1,7 +1,7 @@
|
||||
framework:
|
||||
default_locale: en
|
||||
# Keep in sync with App\Domain\Enum\AppLocale and translations/messages.*.yaml.
|
||||
enabled_locales: ['en', 'de', 'en_PI']
|
||||
enabled_locales: ['en', 'de', 'fr', 'en_PI']
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
providers:
|
||||
@@ -1,4 +1,4 @@
|
||||
<!-- translated-from: features/appearance.md sha1:43f57642347736db71ec013bef9e22bfb70ee747 -->
|
||||
<!-- translated-from: features/appearance.md sha1:963b5496c55b39c78ef14815a4c3c0e5b2ae8f78 -->
|
||||
|
||||
# Darstellung
|
||||
|
||||
@@ -281,7 +281,7 @@ wurde, bevor es ein bestimmtes Bedienelement gab, trotzdem irgendwo Sinnvollem l
|
||||
wird. Deine Mail bleibt genau so, wie sie geschrieben wurde — es wird nichts übersetzt und
|
||||
nichts umgeschrieben.
|
||||
|
||||
plMail liefert **English**, **Deutsch** und **Pirate English** mit. Eine Änderung lädt die
|
||||
plMail liefert **English**, **Deutsch**, **Français** und **Pirate English** mit. Eine Änderung lädt die
|
||||
Seite neu, statt sie nachzubessern, denn jede Zeichenkette auf dem Bildschirm muss neu
|
||||
gerendert werden.
|
||||
|
||||
|
||||
@@ -256,7 +256,7 @@ sensible.
|
||||
**Settings → General → Language** sets the language the interface is shown in. Your mail is left
|
||||
exactly as it was written — nothing is translated, and nothing is rewritten.
|
||||
|
||||
plMail ships **English**, **Deutsch** and **Pirate English**. Changing it reloads the page rather
|
||||
plMail ships **English**, **Deutsch**, **Français** and **Pirate English**. Changing it reloads the page rather
|
||||
than patching it, because every string on screen has to be re-rendered.
|
||||
|
||||
The same section carries **Time zone**, which decides the clock times and dates you are shown. It
|
||||
|
||||
@@ -14,6 +14,7 @@ enum AppLocale: string
|
||||
{
|
||||
case English = 'en';
|
||||
case German = 'de';
|
||||
case French = 'fr';
|
||||
case Pirate = 'en_PI';
|
||||
|
||||
/**
|
||||
@@ -24,6 +25,7 @@ enum AppLocale: string
|
||||
return match ($this) {
|
||||
self::English => 'English',
|
||||
self::German => 'Deutsch',
|
||||
self::French => 'Français',
|
||||
self::Pirate => 'Pirate English',
|
||||
};
|
||||
}
|
||||
@@ -36,6 +38,7 @@ enum AppLocale: string
|
||||
return match ($this) {
|
||||
self::English => '🇬🇧',
|
||||
self::German => '🇩🇪',
|
||||
self::French => '🇫🇷',
|
||||
self::Pirate => '🏴☠️',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ enum ClockFormat: string
|
||||
public static function forLocale(?AppLocale $locale): self
|
||||
{
|
||||
return match ($locale) {
|
||||
AppLocale::German => self::TwentyFour,
|
||||
AppLocale::German,
|
||||
AppLocale::French => self::TwentyFour,
|
||||
default => self::Twelve,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -883,7 +883,7 @@ Variables: form, message, ctx (an App\Domain\DTO\Mail\ComposeContext)
|
||||
<div data-controller="ui--dropdown compose--emoji-picker"
|
||||
data-action="ui--dropdown:opened->compose--emoji-picker#build"
|
||||
data-compose--emoji-picker-locale-value="{{ app.request.locale|split('_')|first }}"
|
||||
data-compose--emoji-picker-data-source-value="{{ asset('emoji/' ~ (app.request.locale starts with 'de' ? 'de' : 'en') ~ '/data.json') }}"
|
||||
data-compose--emoji-picker-data-source-value="{{ asset('emoji/' ~ (app.request.locale|split('_')|first in ['de', 'fr'] ? app.request.locale|split('_')|first : 'en') ~ '/data.json') }}"
|
||||
data-compose--emoji-picker-i18n-value="{{ {
|
||||
categoriesLabel: 'compose.emoji.categories'|trans,
|
||||
emojiUnsupportedMessage: 'compose.emoji.unsupported'|trans,
|
||||
|
||||
@@ -39,7 +39,7 @@ final class EmbeddingPresetTest extends KernelTestCase
|
||||
$translator = self::getContainer()->get(TranslatorInterface::class);
|
||||
|
||||
foreach (EmbeddingPreset::ordered() as $preset) {
|
||||
foreach (['en', 'de', 'en_PI'] as $locale) {
|
||||
foreach (['en', 'de', 'fr', 'en_PI'] as $locale) {
|
||||
$key = $preset->summaryKey();
|
||||
$text = $translator->trans($key, [], 'messages', $locale);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
/**
|
||||
* Every key in the English catalogue exists in German and in pirate English.
|
||||
* Every key in the English catalogue exists in German, French and pirate English.
|
||||
*
|
||||
* A missing key is not an error anywhere Symfony looks: the translator falls
|
||||
* back to `en`, the page renders, and the only symptom is an English sentence
|
||||
@@ -27,6 +27,7 @@ final class CatalogueParityTest extends TestCase
|
||||
public static function locales(): iterable
|
||||
{
|
||||
yield 'de' => ['de'];
|
||||
yield 'fr' => ['fr'];
|
||||
yield 'en_PI' => ['en_PI'];
|
||||
}
|
||||
|
||||
|
||||
@@ -49,8 +49,12 @@ final class ClockFormatResolverTest extends TestCase
|
||||
$english = new User();
|
||||
$english->locale = 'en';
|
||||
|
||||
$french = new User();
|
||||
$french->locale = 'fr';
|
||||
|
||||
self::assertSame(ClockFormat::TwentyFour, $this->resolver()->resolve($german));
|
||||
self::assertSame(ClockFormat::Twelve, $this->resolver()->resolve($english));
|
||||
self::assertSame(ClockFormat::TwentyFour, $this->resolver()->resolve($french), 'French writes 14 h 00, never 2 PM');
|
||||
}
|
||||
|
||||
/** A user with no locale of their own follows the install's. */
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,14 @@
|
||||
# Échecs de connexion, dans le domaine où Symfony les résout. Voir
|
||||
# security.en.yaml pour le pourquoi de chaque entrée.
|
||||
account.deactivated: Votre compte a été désactivé par un administrateur. Demandez-lui de le réactiver — rien de ce qui vous appartient n’a été supprimé.
|
||||
account.removed: Ce compte a été supprimé et ne peut plus servir à se connecter.
|
||||
|
||||
# Authentification unique via un fournisseur OpenID Connect.
|
||||
oidc.unavailable: L’authentification unique ne fonctionne pas pour le moment. Connectez-vous avec votre mot de passe, ou demandez à votre administrateur de consulter les journaux.
|
||||
oidc.denied: Le fournisseur de connexion ne vous a pas laissé entrer dans plMail.
|
||||
oidc.failed: La connexion via votre fournisseur n’a pas pu aboutir. Réessayez.
|
||||
oidc.no_email: 'Votre fournisseur de connexion n’a pas communiqué votre adresse e-mail à plMail : il n’y a donc aucun compte auquel vous connecter.'
|
||||
oidc.email_unverified: Votre fournisseur de connexion n’a pas confirmé votre adresse e-mail, plMail ne la liera donc à aucun compte. Adressez-vous à votre administrateur.
|
||||
oidc.ambiguous: Plusieurs comptes ici ont votre adresse e-mail. Demandez à votre administrateur de régler cela.
|
||||
oidc.no_account: Il n’existe pas encore de compte plMail pour vous. Demandez à votre administrateur d’en créer un avec votre adresse e-mail.
|
||||
oidc.linked_elsewhere: Le compte qui a votre adresse e-mail est déjà lié à quelqu’un d’autre chez votre fournisseur de connexion. Adressez-vous à votre administrateur.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Messages de contraintes. Symfony traduit la validation dans le domaine
|
||||
# `validators`, pas `messages` — voir validators.en.yaml.
|
||||
account.host_invalid: Saisissez uniquement un nom d’hôte ou une adresse IP — sans barre oblique, point d’interrogation ni @.
|
||||
admin.ai.keep_alive_invalid: Utilisez une durée comme 5m, 30m ou 4h, un nombre de secondes, ou -1 pour garder le modèle chargé en permanence — laissez vide pour utiliser le réglage de l’hôte.
|
||||
admin.integrations.client_id_missing: Saisissez l’ID client qui va avec ce secret.
|
||||
admin.integrations.secret_missing: Saisissez aussi le secret client — un ID client seul ne permet à personne de se connecter.
|
||||
admin.sso.error.client_id_missing: 'Un ID client est nécessaire avec l’émetteur et le secret. Les trois, ou aucun.'
|
||||
admin.sso.error.issuer_missing: 'Un émetteur est nécessaire avec l’ID client et le secret. Les trois, ou aucun.'
|
||||
admin.sso.error.issuer_not_https: 'L’émetteur doit commencer par https://. plMail fait confiance au fournisseur grâce à la connexion TLS vers lui.'
|
||||
admin.sso.error.secret_missing: 'Un secret client est nécessaire avec l’émetteur et l’ID client. Les trois, ou aucun.'
|
||||
admin.users.password_too_short: Utilisez au moins {{ limit }} caractères — ce mot de passe est choisi pour quelqu’un d’autre.
|
||||
alias.invalid: Saisissez une adresse e-mail valide.
|
||||
app_password.name_required: Donnez un nom au mot de passe d’application pour le reconnaître plus tard.
|
||||
caldav.address_required: Saisissez l’adresse de votre serveur CalDAV — sans elle, plMail n’a personne à interroger.
|
||||
caldav.credential_required: Saisissez un mot de passe d’application, ou cochez la case pour utiliser celui enregistré d’un compte mail.
|
||||
caldav.reuse_account_required: Choisissez le compte mail dont utiliser le mot de passe, ou décochez la case et saisissez-en un.
|
||||
caldav.username_required: Saisissez le nom d’utilisateur sous lequel votre serveur CalDAV vous connaît.
|
||||
calendar.name_required: Donnez un nom à l’agenda — c’est ce qui le distingue des autres.
|
||||
compose.recipient_invalid: "« {{ address }} » n’est pas une adresse e-mail valide."
|
||||
compose.recipient_required: Ajoutez au moins un destinataire avant d’envoyer.
|
||||
settings.password.current_required: Saisissez votre mot de passe actuel — c’est ce qui empêche quelqu’un devant votre écran déverrouillé de le changer.
|
||||
settings.password.current_wrong: Ce n’est pas votre mot de passe actuel.
|
||||
settings.password.mismatch: Les deux nouveaux mots de passe ne correspondent pas.
|
||||
settings.password.too_short: Utilisez au moins {{ limit }} caractères.
|
||||
setup.install.password_mismatch: Les deux mots de passe ne correspondent pas.
|
||||
setup.install.password_too_short: Utilisez au moins {{ limit }} caractères. Ce compte n’a pas de réinitialisation de mot de passe.
|
||||
user.email_taken: Quelqu’un utilise déjà cette adresse. Chaque adresse de cette installation appartient à une seule personne.
|
||||
admin.ai.field.base_url_invalid: 'Ceci doit commencer par http:// ou https://.'
|
||||
profile.avatar_wrong_type: 'Utilisez une image PNG, JPEG, GIF ou WebP.'
|
||||
admin.ai.headers.malformed: 'Écrivez chaque en-tête sur sa propre ligne sous la forme Nom: valeur.'
|
||||
admin.ai.headers.name: 'Un nom d’en-tête ne peut contenir que des lettres, des chiffres et des tirets, et ne peut pas être vide.'
|
||||
admin.ai.headers.value: 'Une valeur d’en-tête ne peut pas être vide, ne peut pas contenir de saut de ligne ni d’autre caractère de contrôle, et fait au plus 4096 caractères.'
|
||||
admin.ai.headers.reserved: 'Un de ces en-têtes est défini par plMail lui-même et ne peut pas être remplacé : Host, Content-Type, Content-Length, Connection et consorts.'
|
||||
admin.ai.headers.duplicate: 'Le même en-tête est listé deux fois.'
|
||||
admin.ai.headers.too_many: 'Au plus %max% en-têtes supplémentaires.'
|
||||
Reference in new issue
Block a user