fix(library): default library name is now locale-aware, not the OS user name

defaultLibrary() no longer falls back to System.getProperty("user.name")
-- AppDirectories.dataDir() is already namespaced per OS user profile on
every platform, so that dedup never actually protected anything. It now
reads a friendly name ("My Photos" / "Mes photos") straight off the
messages* bundles for Locale.getDefault(), since no Spring context (and
so no I18nService) exists yet at this point in the bootstrap.

sanitize() now leaves a space untouched (safe in both a filename and the
JDBC URL) so "My Photos" survives as a real space instead of turning
into "My_Photos".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 18:08:42 -04:00
co-authored by Claude Sonnet 5
parent 9702de0018
commit 40319bf7dd
4 changed files with 51 additions and 13 deletions
@@ -10,6 +10,9 @@ import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.stream.Stream;
/**
@@ -31,9 +34,18 @@ public final class LibraryCatalog {
static final String DATABASE_SUFFIX = ".mv.db";
/**
* Used when {@code user.name} is unusable, so a library always has a name.
* Used when a name is blank, and as {@link #defaultLibrary()}'s own ultimate fallback when no
* translation is available for the current locale.
*/
static final String FALLBACK_NAME = "default";
static final String FALLBACK_NAME = "My Photos";
/**
* Message key for {@link #defaultLibrary()}'s own name, read straight from the {@code messages*
* .properties} bundles Spring's {@code MessageSource} is itself built from — {@link #defaultLibrary()}
* runs before there is a Spring context (see its own javadoc), so it cannot go through
* {@code I18nService}, only around it.
*/
private static final String DEFAULT_NAME_KEY = "library.defaultName";
private static final Logger log = LoggerFactory.getLogger(LibraryCatalog.class);
@@ -76,10 +88,12 @@ public final class LibraryCatalog {
/**
* Reduces {@code name} to {@code [A-Za-z0-9._-]}, replacing everything else with {@code _}.
*
* <p>Not cosmetic. The default library name comes from {@code user.name}, which the OS does not
* constrain: a {@code /} would let the database file escape the data directory, and a {@code ;} would
* append parameters to the JDBC URL the name is interpolated into. Leading dots are stripped too, so a
* name can never produce a hidden file or a {@code ..} traversal.
* <p>Not cosmetic. A user-supplied library name is not otherwise constrained: a {@code /} would let the
* database file escape the data directory, and a {@code ;} would append parameters to the JDBC URL the
* name is interpolated into. Leading dots are stripped too, so a name can never produce a hidden file or
* a {@code ..} traversal. A space is left untouched — safe on every supported OS's filesystem and in the
* JDBC URL alike — so {@link #defaultLibrary()}'s own "My Photos" survives as a real space rather than
* turning into "My_Photos".
*/
public static String sanitize(@Nullable String name) {
if (name == null || name.isBlank()) {
@@ -98,11 +112,26 @@ public final class LibraryCatalog {
}
/**
* The library opened when nothing has been chosen yet: the OS user name, so two accounts on one
* machine do not share an index.
* The library opened when nothing has been chosen yet: a friendly, locale-appropriate name — not the
* OS user name this used to be. {@link AppDirectories#dataDir()} is already namespaced per OS user
* profile (macOS/Windows/Linux each resolve it under that user's own home or app-data directory), so a
* fixed name here never actually risked two accounts on one machine sharing an index.
*
* <p>Resolved directly from the {@code messages} resource bundles against {@link Locale#getDefault()}
* — the OS/JVM locale, since no {@code I18nService} (and so no user-chosen UI language) exists this
* early — falling back to {@link #FALLBACK_NAME} if the bundle or key is ever missing.
*/
public static String defaultLibrary() {
return sanitize(System.getProperty("user.name", FALLBACK_NAME));
return sanitize(localizedDefaultName());
}
private static String localizedDefaultName() {
try {
return ResourceBundle.getBundle("messages", Locale.getDefault()).getString(DEFAULT_NAME_KEY);
}
catch (MissingResourceException e) {
return FALLBACK_NAME;
}
}
/**
@@ -138,6 +167,6 @@ public final class LibraryCatalog {
return (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
|| c == '.' || c == '_' || c == '-';
|| c == '.' || c == '_' || c == '-' || c == ' ';
}
}
+3
View File
@@ -22,6 +22,9 @@ nav.maintenance.recognition=People & Pets
nav.drawer.empty=Nothing here yet
header.import.tooltip=Import from a card or external drive (coming soon)
# --- Library folders (Photothèque drawer) ---
# Read directly off the bundle by LibraryCatalog.defaultLibrary(), not through I18nService — see its own
# javadoc for why (no Spring context exists yet at that point).
library.defaultName=My Photos
library.addFolder.tooltip=Monitor an existing folder.
library.chooseFolder.dialogTitle=Choose a folder to add
library.removeFolder.tooltip=Remove this folder
@@ -22,6 +22,9 @@ nav.maintenance.recognition=Personnes et animaux
nav.drawer.empty=Rien ici pour l'instant
header.import.tooltip=Importer depuis une carte ou un disque externe (bientôt disponible)
# --- Dossiers de la photothèque (tiroir) ---
# Lu directement dans le fichier de traductions par LibraryCatalog.defaultLibrary(), sans passer par
# I18nService — voir sa propre javadoc (aucun contexte Spring n'existe encore à ce stade).
library.defaultName=Mes photos
library.addFolder.tooltip=Surveiller un dossier existant.
library.chooseFolder.dialogTitle=Choisir un dossier à ajouter
library.removeFolder.tooltip=Retirer ce dossier
@@ -59,9 +59,9 @@ class LibraryCatalogTest {
}
/**
* The default name comes from {@code user.name}, which the OS does not constrain. A separator would let
* the database file escape the data directory and a semicolon would append parameters to the JDBC URL
* the name is interpolated into.
* A user-supplied library name is not otherwise constrained. A separator would let the database file
* escape the data directory and a semicolon would append parameters to the JDBC URL the name is
* interpolated into.
*/
@Test
void sanitizesNamesThatWouldEscapeTheDirectoryOrTheUrl() {
@@ -78,6 +78,9 @@ class LibraryCatalogTest {
softly.assertThat(LibraryCatalog.sanitize(" ")).isEqualTo(LibraryCatalog.FALLBACK_NAME);
softly.assertThat(LibraryCatalog.sanitize(null)).isEqualTo(LibraryCatalog.FALLBACK_NAME);
softly.assertThat(LibraryCatalog.sanitize("my-photos_2024.v2")).isEqualTo("my-photos_2024.v2");
// A space is safe on every supported OS's filesystem and in the JDBC URL alike, so it survives
// untouched — this is what keeps defaultLibrary()'s own "My Photos" a real space, not "My_Photos".
softly.assertThat(LibraryCatalog.sanitize("My Photos")).isEqualTo("My Photos");
});
}