i18n(android): the interface speaks English, like the desktop app

It was in French because the conversation is, and I never asked. The desktop
app the client mirrors says Add field, Attach file, Show previous versions -
someone moving between the two would notice the seam.

Every visible string moved: buttons, statuses, detail row captions, log lines
and the biometric prompt, plus the messages the modules surface through
LastStoreError and LastBiometricError.

Code comments stay in French. They address the author, not the user, and that
is already the language of CLAUDE.md and much of the repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-08-15 17:59:31 +01:00
parent b900b5b609
commit bc89ec0010
5 changed files with 85 additions and 85 deletions
+6 -6
View File
@@ -104,12 +104,12 @@ begin
try
if not AcquireService(LSvc) then
begin
FLastError := 'service biometrique absent de la plateforme';
FLastError := 'biometric service missing from the platform';
Exit(False);
end;
Result := LSvc.CanAuthenticate;
if not Result then
FLastError := 'CanAuthenticate: aucune protection utilisable';
FLastError := 'CanAuthenticate: no usable protection';
except
on E: Exception do
begin
@@ -136,7 +136,7 @@ begin
FLastError := '';
if not AcquireService(LSvc) then
begin
FLastError := 'service biometrique absent de la plateforme';
FLastError := 'biometric service missing from the platform';
AThen(boFailed); // pas boUnavailable : on ne sait rien de l'appareil
Exit;
end;
@@ -144,7 +144,7 @@ begin
if not LSvc.CanAuthenticate then
begin
// Reponse NETTE du systeme : l'appareil n'a rien pour authentifier.
FLastError := 'CanAuthenticate: aucune protection utilisable';
FLastError := 'CanAuthenticate: no usable protection';
AThen(boUnavailable);
Exit;
end;
@@ -158,9 +158,9 @@ begin
end;
LProps := Default(TBiometricPromptProperties);
LProps.Title := 'Deverrouiller le coffre';
LProps.Title := 'Unlock the vault';
LProps.Description := APrompt;
LProps.CancelButtonText := 'Annuler';
LProps.CancelButtonText := 'Cancel';
LSvc.SetPromptProperties(LProps);
// Meme raison que plus haut : ce qui remonte de la couche Java doit
+1 -1
View File
@@ -68,7 +68,7 @@ begin
// contenu, seulement son origine.
// Les DEUX arguments sont des CharSequence : passer un JString au premier
// compile ailleurs mais pas ici.
LClip := TJClipData.JavaClass.newPlainText(StrToJCharSequence('coffre'),
LClip := TJClipData.JavaClass.newPlainText(StrToJCharSequence('vault'),
StrToJCharSequence(AText));
LExtras := TJPersistableBundle.JavaClass.init;
LExtras.putBoolean(StringToJString(EXTRA_IS_SENSITIVE), True);
+2 -2
View File
@@ -594,7 +594,7 @@ begin
LStore := OpenKeyStore;
LCert := LStore.getCertificate(StringToJString(KEY_ALIAS_WRAP));
if LCert = nil then
raise EPMAStore.Create('Cle publique introuvable dans le Keystore');
raise EPMAStore.Create('Public key not found in the Keystore');
LPub := LCert.getPublicKey;
LFactory := TJKeyFactoryX.JavaClass.getInstance(LPub.getAlgorithm);
Result := LFactory.generatePublic(
@@ -804,7 +804,7 @@ begin
end;
{$ELSE}
begin
raise EPMAStore.Create('Le stockage Keystore est propre a Android');
raise EPMAStore.Create('Keystore storage is Android-only');
end;
{$ENDIF}
+3 -3
View File
@@ -110,7 +110,7 @@ begin
Result := Title.Trim;
if Result = '' then Result := Site.Trim;
if Result = '' then Result := Username.Trim;
if Result = '' then Result := '(sans nom)';
if Result = '' then Result := '(no name)';
end;
{ TVault }
@@ -206,7 +206,7 @@ begin
Clear;
LRoot := TJSONObject.ParseJSONValue(APlainJson) as TJSONObject;
if LRoot = nil then
raise Exception.Create('Instantane illisible : ce n''est pas du JSON');
raise Exception.Create('Snapshot unreadable: this is not JSON');
try
FUsername := LRoot.GetValue<string>('username', '');
FExportedAt := LRoot.GetValue<string>('exported_at',
@@ -228,7 +228,7 @@ begin
end;
if not LRoot.TryGetValue<TJSONArray>('entries', LArr) then
raise Exception.Create('Instantane sans tableau "entries"');
raise Exception.Create('Snapshot has no "entries" array');
SetLength(FEntries, LArr.Count);
N := 0;
+73 -73
View File
@@ -1,4 +1,4 @@
unit mainApk;
unit mainApk;
{
Client Android en LECTURE SEULE : deverrouiller, chercher, copier.
@@ -218,7 +218,7 @@ var
LEvents: IFMXApplicationEventService;
begin
inherited;
Caption := 'Coffre';
Caption := 'Vault';
// Le fond de la forme, et non celui du style : tout le reste est peint
// par-dessus, et un gris herite se verrait entre les blocs.
Fill.Kind := TBrushKind.Solid;
@@ -328,8 +328,8 @@ begin
FLaySettings.Align := TAlignLayout.Top;
FLaySettings.Height := 116;
FLaySettings.Visible := False;
FToken := TopEdit(FLaySettings, 'jeton Bearer', FIRSTRUN_TOKEN);
FUrl := TopEdit(FLaySettings, 'URL de l''endpoint', FIRSTRUN_URL);
FToken := TopEdit(FLaySettings, 'bearer token', FIRSTRUN_TOKEN);
FUrl := TopEdit(FLaySettings, 'endpoint URL', FIRSTRUN_URL);
FToken.Margins.Rect := RectF(16, 8, 16, 0);
FUrl.Margins.Rect := RectF(16, 8, 16, 0);
// Ce que l'appareil a retenu l'emporte sur les valeurs de premier lancement.
@@ -347,20 +347,20 @@ begin
LRow.Height := 48;
LRow.Margins.Rect := RectF(16, 8, 16, 0);
FBtnLog := FlatButton(LRow, 'Journal', OnToggleLog);
FBtnLog := FlatButton(LRow, 'Log', OnToggleLog);
FBtnLog.Align := TAlignLayout.Right;
FBtnLog.Width := 120;
FBtnLog.Margins.Left := 8;
FBtnSettings := FlatButton(LRow, 'Reglages du serveur', OnToggleSettings);
FBtnSettings := FlatButton(LRow, 'Server settings', OnToggleSettings);
FBtnSettings.Align := TAlignLayout.Client;
FBtnSync := TopButton(FLayUnlock, 'Synchroniser', DoSync);
FBtnSync := TopButton(FLayUnlock, 'Sync', DoSync);
// Accentue : c'est l'action attendue quand aucun coffre n'est encore la.
FBtnOpen := TopButton(FLayUnlock, 'Ouvrir le coffre', DoOpenLocal, 52, True);
FBtnOpen := TopButton(FLayUnlock, 'Open vault', DoOpenLocal, 52, True);
// Pre-rempli depuis le fichier local, jamais depuis ce fichier-ci.
FPwd := TopEdit(FLayUnlock, 'mot de passe de sync', TEST_PWD, True);
FPwd := TopEdit(FLayUnlock, 'sync password', TEST_PWD, True);
FPwd.Margins.Rect := RectF(16, 16, 16, 0);
FStatus := TLabel.Create(Self);
@@ -386,7 +386,7 @@ begin
LBar.Align := TAlignLayout.Bottom;
LBar.Height := 52;
LBtnLock := FlatButton(LBar, 'Verrouiller', OnBackTap);
LBtnLock := FlatButton(LBar, 'Lock', OnBackTap);
LBtnLock.Align := TAlignLayout.Left;
LBtnLock.Margins.Rect := RectF(16, 4, 0, 8);
LBtnLock.Width := 140;
@@ -422,7 +422,7 @@ begin
//
// Bouton peint et non TComboBox : le combo etait l'un des deux derniers
// controles a porter encore le style, et il jurait avec le reste.
FBtnFolder := FlatButton(LTop, 'Tous les dossiers', OnFolderTap);
FBtnFolder := FlatButton(LTop, 'All folders', OnFolderTap);
FBtnFolder.Align := TAlignLayout.Right;
FBtnFolder.Width := 170;
FBtnFolder.Margins.Left := 8;
@@ -445,7 +445,7 @@ begin
FSearchHint.Margins.Left := 10;
FSearchHint.HitTest := False;
FSearchHint.Opacity := 0.45;
FSearchHint.Text := 'Rechercher';
FSearchHint.Text := 'Search';
end;
/// Intitule discret au-dessus d'une valeur. Meme forme pour toutes les
@@ -504,7 +504,7 @@ begin
FLayDetail.Align := TAlignLayout.Client;
FLayDetail.Visible := False;
LBtnBack := FlatButton(FLayDetail, 'Retour', OnBackTap);
LBtnBack := FlatButton(FLayDetail, 'Back', OnBackTap);
LBtnBack.Align := TAlignLayout.Bottom;
LBtnBack.Height := 52;
LBtnBack.Margins.Rect := RectF(16, 8, 16, 8);
@@ -547,15 +547,15 @@ begin
if HasVault then
begin
if VaultNeedsAuth then
Status('Coffre present sur cet appareil - protege')
Status('Vault on this device - protected')
else
Status('Coffre present sur cet appareil');
SetButtonText(FBtnSync, 'Synchroniser');
Status('Vault on this device');
SetButtonText(FBtnSync, 'Sync');
end
else
begin
Status('Aucun coffre local - premier telechargement');
SetButtonText(FBtnSync, 'Telecharger le coffre');
Status('No local vault - first download');
SetButtonText(FBtnSync, 'Download vault');
end;
end;
@@ -635,10 +635,10 @@ begin
// Le corps d'une note est stocke dans le champ mot de passe : meme donnee,
// autre intitule, et pas de raison de la masquer comme un secret.
if FCurrent.IsNote then LPwdCaption := 'Contenu' else LPwdCaption := 'Mot de passe';
if FCurrent.IsNote then LPwdCaption := 'Content' else LPwdCaption := 'Password';
Add('Site', FCurrent.Site, False);
Add('Identifiant', FCurrent.Username, False);
Add('Username', FCurrent.Username, False);
Add(LPwdCaption, FCurrent.Password, not FCurrent.IsNote);
for I := 0 to High(FCurrent.Fields) do
Add(FCurrent.Fields[I].Caption, FCurrent.Fields[I].Value,
@@ -658,16 +658,16 @@ begin
// rendu bidirectionnel de FMX en fait n'importe quoi. Separes, chacun se
// lit dans son sens.
for I := 0 to High(FCurrent.Attachments) do
Add('Piece jointe - ' + HumanSize(FCurrent.Attachments[I].SizeBytes),
Add('Attachment - ' + HumanSize(FCurrent.Attachments[I].SizeBytes),
FCurrent.Attachments[I].FileName, False);
Add('Dossier / etiquettes', LMeta, False);
Add('Folder / tags', LMeta, False);
// Le sous-type n'existe que pour les modeles (carte, cle SSH, serveur...) ;
// Add ignore une valeur vide, donc rien a tester ici.
Add('Type', FCurrent.Template, False);
// Horodatage tel quel : c'est de l'ISO 8601 UTC, lisible, et le reformater
// demanderait de decider d'un fuseau alors qu'il n'y a rien a arbitrer.
Add('Modifie le', FCurrent.UpdatedAt, False);
Add('Updated', FCurrent.UpdatedAt, False);
SetLength(FRowValues, Length(FRows));
SetLength(FRowShown, Length(FRows));
@@ -678,12 +678,12 @@ begin
LOrder := 0;
if FCurrent.Totp <> '' then
begin
LRow := MakeRow(FDetBody, 'Code 2FA', LOrder, FTotpValue);
LRow := MakeRow(FDetBody, '2FA code', LOrder, FTotpValue);
Inc(LOrder);
SetLength(FRowCtls, Length(FRowCtls) + 1);
FRowCtls[High(FRowCtls)] := LRow;
FTotpValue.TextSettings.Font.Size := 26;
AddRowButton(LRow, 'Copier', -1, OnCopyTap);
AddRowButton(LRow, 'Copy', -1, OnCopyTap);
OnTotpTick(nil); // pas d'attente d'une seconde a l'ouverture
FTotpTimer.Enabled := True;
end;
@@ -698,11 +698,11 @@ begin
if FRows[I].Secret then
begin
LValue.Text := MASK;
AddRowButton(LRow, 'Afficher', I, OnRevealTap);
AddRowButton(LRow, 'Show', I, OnRevealTap);
end
else
LValue.Text := FRows[I].Value;
AddRowButton(LRow, 'Copier', I, OnCopyTap);
AddRowButton(LRow, 'Copy', I, OnCopyTap);
end;
// La derniere ligne affichee touche sinon le bord du defilement, ce qui la
// fait passer pour tronquee alors qu'elle est juste collee.
@@ -728,7 +728,7 @@ begin
if FTotpCode = '' then
begin
// Un secret illisible ne doit pas passer pour un code : le dire.
FTotpValue.Text := 'secret illisible';
FTotpValue.Text := 'unreadable secret';
FTotpTimer.Enabled := False;
Exit;
end;
@@ -810,8 +810,8 @@ end;
procedure TForm8.AdoptVault(const APlainJson: string);
begin
FVault.LoadFromJson(APlainJson);
Log(' ' + FVault.LiveCount.ToString + ' entrees, ' +
FVault.TrashCount.ToString + ' en corbeille');
Log(' ' + FVault.LiveCount.ToString + ' entries, ' +
FVault.TrashCount.ToString + ' in trash');
RebuildIcons;
PopulateFolders;
Touch;
@@ -845,7 +845,7 @@ begin
on E: Exception do
begin
// Un coffre local illisible se remplace, il ne se repare pas.
Log('Coffre local corrompu : ' + E.Message);
Log('Local vault corrupt: ' + E.Message);
ClearVault;
ShowUnlock;
end;
@@ -856,8 +856,8 @@ begin
// ShowUnlock AVANT le message : il repositionne le statut selon ce que
// l'appareil detient, et ecraserait donc l'explication.
ShowUnlock;
Log('Coffre local illisible : ' + LastStoreError);
Status('Coffre local illisible - voir le journal');
Log('Local vault unreadable: ' + LastStoreError);
Status('Local vault unreadable - see log');
end;
end;
@@ -877,13 +877,13 @@ begin
Exit;
end;
if not AAuto then Status('Authentification...');
AuthenticateThen('Acceder au coffre',
if not AAuto then Status('Authenticating...');
AuthenticateThen('Unlock the vault',
procedure(AOutcome: TBioOutcome)
begin
case AOutcome of
boOK: OpenLocalNow;
boCancelled: Status('Annule');
boCancelled: Status('Cancelled');
boUnavailable:
// Reponse nette du systeme : plus rien pour authentifier sur cet
// appareil, donc la cle est perdue et le cache avec elle.
@@ -895,13 +895,13 @@ begin
// masquerait la vraie cause. L'utilisateur tranche en reessayant.
if AAuto then
begin
Log('Authentification indisponible au lancement - cache conserve');
Log('Authentication unavailable at launch - cache kept');
Log(' ' + LastBiometricError);
Status('Authentification indisponible - reessayer');
Status('Authentication unavailable - try again');
end
else
begin
Log('Plus aucune protection sur l''appareil - cache efface');
Log('No protection left on this device - cache cleared');
Log(' ' + LastBiometricError);
ClearVault;
ShowUnlock;
@@ -909,8 +909,8 @@ begin
else
// boFailed : la couche Java a leve quelque chose. Cela ne dit rien de
// l'appareil, donc on ne touche a rien - on rapporte.
Log('Echec de l''authentification : ' + LastBiometricError);
Status('Echec de l''authentification');
Log('Authentication failed: ' + LastBiometricError);
Status('Authentication failed');
end;
end);
end;
@@ -930,7 +930,7 @@ begin
if LPwd = '' then LPwd := FSyncPwd;
if LPwd = '' then
begin
Status('Mot de passe de sync requis');
Status('Sync password required');
Exit;
end;
FLog.Lines.Clear;
@@ -942,16 +942,16 @@ begin
// cet appareil - c'est lui qui exprime le choix de l'utilisateur.
if VaultNeedsAuth and (FVault.Count = 0) then
begin
Status('Authentification...');
AuthenticateThen('Acceder au coffre',
Status('Authenticating...');
AuthenticateThen('Unlock the vault',
procedure(AOutcome: TBioOutcome)
begin
if AOutcome = boOK then
FetchAndDecrypt(LUrl, LTok, LPwd)
else
begin
Log('Synchronisation refusee : ' + LastBiometricError);
Status('Authentification requise');
Log('Sync refused: ' + LastBiometricError);
Status('Authentication required');
end;
end);
Exit;
@@ -966,7 +966,7 @@ var
LCanAuth: Boolean;
begin
SetBusy(True);
if not ASilent then Status('Telechargement...');
if not ASilent then Status('Downloading...');
// NE JAMAIS FERMER LE CACHE DERRIERE UNE PORTE QU'ON NE SAIT PAS OUVRIR.
// Demander une cle protegee par authentification alors que l'invite ne peut
// pas s'afficher (permission absente, aucune protection configuree) produit
@@ -997,7 +997,7 @@ begin
LSw := TStopwatch.StartNew;
LRes := LClient.Fetch(FETag);
LSw.Stop;
Log(' HTTP ' + LRes.HttpCode.ToString + ' en ' +
Log(' HTTP ' + LRes.HttpCode.ToString + ' in ' +
LSw.ElapsedMilliseconds.ToString + ' ms');
case LRes.Status of
@@ -1018,14 +1018,14 @@ begin
// rafraichissement de fond c'est le cas courant, et il
// doit passer inapercu - surtout pas de retour a la liste
// alors que l'utilisateur consulte une entree.
Log(' 304 : inchange, coffre deja ouvert')
Log(' 304: unchanged, vault already open')
else if LCached <> '' then
begin
Log(' 304 : inchange, coffre servi depuis le cache');
Log(' 304: unchanged, vault served from cache');
try
AdoptVault(LCached);
except
on E: Exception do Status('Cache illisible : ' + E.Message);
on E: Exception do Status('Cache unreadable: ' + E.Message);
end;
end
else
@@ -1034,7 +1034,7 @@ begin
// On le jette pour que l'appel suivant retelecharge.
FETag := '';
if not ASilent then
Status('Cache absent - appuyer a nouveau pour telecharger');
Status('No cache - tap again to download');
end;
end);
Exit;
@@ -1042,10 +1042,10 @@ begin
ssNoSnapshot:
begin
// Le serveur repond pareil dans les deux cas, par conception.
Log(' 404 : pas d''instantane OU jeton invalide');
Log(' 404: no snapshot OR invalid token');
if not ASilent then
TThread.Queue(nil, procedure
begin Status('Rien a telecharger (404)'); end);
begin Status('Nothing to download (404)'); end);
Exit;
end;
ssRefused, ssHttpError, ssNetworkError:
@@ -1053,17 +1053,17 @@ begin
// En fond, un echec reseau ne se signale pas : le coffre local
// est affiche, hors ligne est un etat normal, et une alerte ici
// apprendrait a l'utilisateur a ignorer les alertes.
Log(' echec : ' + LRes.Error);
Log(' failed: ' + LRes.Error);
if not ASilent then
TThread.Queue(nil, procedure
begin Status('Echec reseau'); end);
begin Status('Network failure'); end);
Exit;
end;
end;
Log(' recu ' + Length(LRes.Container).ToString + ' octets');
Log(' received ' + Length(LRes.Container).ToString + ' bytes');
if not ASilent then
TThread.Queue(nil, procedure begin Status('Dechiffrement...'); end);
TThread.Queue(nil, procedure begin Status('Decrypting...'); end);
LSw := TStopwatch.StartNew;
// Volontairement AVANT de retenir l'ETag : un echec ici doit laisser
@@ -1071,7 +1071,7 @@ begin
LPlain := DecryptContainer(LRes.Container, APwd);
LSw.Stop;
FETag := LRes.ETag;
Log(' dechiffre en ' + LSw.ElapsedMilliseconds.ToString + ' ms');
Log(' decrypted in ' + LSw.ElapsedMilliseconds.ToString + ' ms');
// Ecrit APRES un dechiffrement reussi seulement, et avec son ETag :
// les deux doivent rester coherents.
@@ -1083,16 +1083,16 @@ begin
// pas des reglages saisis, on enregistre des reglages EPROUVES.
SaveDeviceSettings(AUrl, ATok);
if VaultNeedsAuth then
Log(' cache ecrit - protege par biometrie/code')
Log(' cache written - protected by biometrics/PIN')
else if LCanAuth then
Log(' cache ecrit - cle simple (Keystore a refuse la contrainte)')
Log(' cache written - plain key (Keystore refused the constraint)')
else
Log(' cache ecrit - AUCUNE protection : pas d''authentification ici');
Log(' cache written - NO protection: no authentication here');
except
// La classe compte autant que le message : "methode introuvable"
// designe une signature JNI fausse, pas un probleme de stockage.
on E: Exception do
Log(' cache non ecrit : ' + E.ClassName + ' - ' + E.Message);
Log(' cache not written: ' + E.ClassName + ' - ' + E.Message);
end;
TThread.Queue(nil,
@@ -1104,13 +1104,13 @@ begin
except
on E: Exception do
if not ASilent then
Status('Instantane illisible : ' + E.Message);
Status('Snapshot unreadable: ' + E.Message);
end;
end);
except
on E: Exception do
begin
Log('ERREUR ' + E.ClassName + ' : ' + E.Message);
Log('ERROR ' + E.ClassName + ' : ' + E.Message);
// Le cas de loin le plus frequent, et le message par defaut ne le
// dit pas : GCM echoue a la verification quand la cle est fausse.
//
@@ -1120,7 +1120,7 @@ begin
// le champ prerempli, et le coffre local reste consultable.
if not ASilent then
TThread.Queue(nil, procedure
begin Status('Echec - mot de passe de sync incorrect ?'); end);
begin Status('Failed - wrong sync password?'); end);
end;
end;
finally
@@ -1188,7 +1188,7 @@ begin
LPrev := FFolderNames[FFolderSel];
SetLength(FFolderNames, 1);
FFolderNames[0] := 'Tous les dossiers';
FFolderNames[0] := 'All folders';
for LF in FVault.Folders do
begin
SetLength(FFolderNames, Length(FFolderNames) + 1);
@@ -1341,7 +1341,7 @@ begin
end;
if (FSearch.Text.Trim = '') and (LFolder = '') then
FCount.Text := FVault.LiveCount.ToString + ' entrees'
FCount.Text := FVault.LiveCount.ToString + ' entries'
else
FCount.Text := Length(FFiltered).ToString + ' / ' +
FVault.LiveCount.ToString;
@@ -1369,12 +1369,12 @@ begin
if FRowShown[I] then
begin
FRowValues[I].Text := FRows[I].Value;
SetButtonText(TRectangle(Sender), 'Masquer');
SetButtonText(TRectangle(Sender), 'Hide');
end
else
begin
FRowValues[I].Text := MASK;
SetButtonText(TRectangle(Sender), 'Afficher');
SetButtonText(TRectangle(Sender), 'Show');
end;
end;
@@ -1388,7 +1388,7 @@ begin
// -1 : la ligne du TOTP, qui n'est pas dans FRows - sa valeur change chaque
// periode et ne vit que dans FTotpCode. Copie sans l'espace d'affichage.
if I = -1 then
CopyToClipboard(FTotpCode, 'Code 2FA')
CopyToClipboard(FTotpCode, '2FA code')
else if (I >= 0) and (I <= High(FRows)) then
CopyToClipboard(FRows[I].Value, FRows[I].Caption);
end;
@@ -1408,7 +1408,7 @@ begin
// ponytail: le message reste jusqu'a la prochaine navigation, pas de minuterie
// pour l'effacer. Un deuxieme TTimer pour deux secondes d'affichage serait
// plus de code que ce que ca apporte.
FDetToast.Text := ALabel + ' copie - presse-papiers efface dans ' +
FDetToast.Text := ALabel + ' copied - clipboard cleared in ' +
CLIPBOARD_CLEAR_SECONDS.ToString + ' s';
end;
@@ -1477,7 +1477,7 @@ begin
end;
if MinutesBetween(Now, FLastActivity) >= AUTOLOCK_MINUTES then
begin
Log('Verrouille apres ' + AUTOLOCK_MINUTES.ToString + ' min d''inactivite');
Log('Locked after ' + AUTOLOCK_MINUTES.ToString + ' min idle');
LockVault;
end;
end;