Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
319 changes: 319 additions & 0 deletions design-doc/3.2-language-selector.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions distribution/doc/release-notes/3.2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ A release about getting out of your way. The improvements below smooth over the
- **Preferences dialog gains a _General_ tab** gathering the signing-engine selection and the new `debug` toggle; both apply immediately.
- **Visible signature images keep their aspect ratio with the DSS engine** — background and graphic images were previously stretched to fill the signature box and came out distorted. A `--bg-scale` of zero still stretches to fill; any other value fits the image and centers it, matching the OpenPDF engine. See issue 460.
- **Clear list in the Recent files menu** — the File menu's recent-files trail can now be emptied on its own, which previously required a factory reset that discarded every other setting as well. The item appears only when there is something to clear. See issue 453.
- **Pick the interface language** — a new _Language_ selector on the _General_ tab of Preferences lets you choose the UI language explicitly instead of always following the operating-system locale; _System default_ stays the default. It is stored as `ui.language` in `advanced.properties` and works on the command line too (`-o ui.language=de`, e.g. to read `--help` in German). The setting is read at startup, so restart to apply it, and it affects interface text only — number/date formatting and the signed output are unchanged. See issue 444.
- **Norwegian translation is now loaded** — the bundled Norwegian Bokmål translation shipped under a file name (`messages_nb-NO.properties`) that Java's resource loader never matched, so it silently rendered English. Renaming it to `messages_nb.properties` makes it apply for both `nb` and `nb-NO` locales.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ public static boolean relaxSslSecurity() {
return cfg().getAsBool("relax.ssl.security", false);
}

/**
* BCP-47 language tag of the user interface ({@code ui.language} in {@code advanced.properties}), or {@code null}
* when unset. Empty / unset means "follow the OS locale". Read once at startup by {@link UiLocale}.
*/
public static String uiLanguage() {
return cfg().getProperty(UiLocale.KEY);
}

/**
* Whether verbose signing diagnostics are enabled ({@code debug} in {@code advanced.properties}). When on,
* the signing pipeline logs the certificate chain, the loaded trust anchors, and every TSA / AIA / CRL /
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*/
public class ResourceProvider {

private ResourceBundle bundle;
private volatile ResourceBundle bundle;

/**
* Constructor which takes a not-<code>null</code> {@link ResourceBundle} as an argument.
Expand All @@ -38,6 +38,20 @@ public ResourceProvider(ResourceBundle bundle) {
this.bundle = bundle;
}

/**
* Swaps the active {@link ResourceBundle}. Called once at startup by {@link UiLocale#init(String[])}, which owns
* the bundle base name and the fallback policy, so every static-import call site keeps reading through this same
* shared instance.
*
* @param newBundle the bundle to read from, not-<code>null</code>
*/
public void reload(final ResourceBundle newBundle) {
if (newBundle == null) {
throw new IllegalArgumentException("ResourceBundle must be not-null.");
}
this.bundle = newBundle;
}

/**
* Sets translations and mnemonics for labels and different kind of buttons
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package net.sf.jsignpdf.utils;

import java.util.List;
import java.util.Locale;

/**
* The bundled UI translations, as BCP-47 language tags. Kept as a static list (not a classpath scan) so it works
* unchanged from a jar, a jlink image or a jpackage bundle. {@code SupportedLanguagesTest} guards it against the
* actual {@code messages*.properties} files.
*/
public final class SupportedLanguages {

/** English (the base bundle) first; the rest sorted by tag. Region kept only where it disambiguates (zh). */
private static final List<String> TAGS = List.of(
"en", "cs", "de", "el", "es", "fr", "hr", "hu", "hy", "it",
"ja", "nb", "pl", "pt", "ru", "sk", "ta", "zh-CN", "zh-TW");

private SupportedLanguages() {
}

/** BCP-47 tags of the bundled translations, English first. */
public static List<String> tags() {
return TAGS;
}

/**
* Whether a translation exists for the given locale. Matches the exact language+region tag (so {@code zh-CN}
* resolves) and, failing that, the language alone (so {@code nb-NO} or {@code pt-BR} resolve to {@code nb} / {@code pt}).
*/
public static boolean isSupported(Locale locale) {
if (locale == null) {
return false;
}
return TAGS.contains(locale.toLanguageTag()) || TAGS.contains(locale.getLanguage());
}

/**
* Label for the selector: native name plus English name, e.g. {@code "Deutsch (German)"}. The English part keeps
* the list readable to someone who cannot read the current UI language — the scenario behind issue #444. Names
* carrying a region already bracket it, so those get an en-dash separator ({@code "中文 (中国) – Chinese (China)"})
* rather than a second, nested pair of parentheses.
*/
public static String displayName(Locale locale) {
String self = locale.getDisplayName(locale);
String english = locale.getDisplayName(Locale.ENGLISH);
if (self.isEmpty()) {
return english;
}
if (self.equalsIgnoreCase(english)) {
return self;
}
if (self.indexOf('(') >= 0 || english.indexOf('(') >= 0) {
return self + " – " + english;
}
return self + " (" + english + ")";
}
}
175 changes: 175 additions & 0 deletions engines/api/src/main/java/net/sf/jsignpdf/utils/UiLocale.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package net.sf.jsignpdf.utils;

import java.util.Locale;
import java.util.ResourceBundle;
import java.util.logging.Level;

import net.sf.jsignpdf.Constants;

/**
* Resolves the configured UI locale ({@code ui.language} in the advanced configuration) and installs it before any
* translated string is read. {@link #init(String[])} must run first thing in {@code main()}, because
* {@link Constants#RES} and the commons-cli option descriptions capture strings during class initialization.
*
* <p>The choice is installed as the base default plus {@link Locale.Category#DISPLAY}, while
* {@link Locale.Category#FORMAT} is pinned to the OS locale: UI text follows the choice while number/date formatting
* keeps following the OS locale, so a UI-language preference can never change signed output. The base default matters
* because {@code ResourceBundle.getBundle(baseName)} without an explicit locale resolves against
* {@link Locale#getDefault()} and ignores the DISPLAY category — that is how JavaFX loads its own control strings
* (the OK / Cancel labels of {@code ButtonType}, text-field context menus), which would otherwise stay in the OS
* language and produce a half-translated UI.
*/
public final class UiLocale {

/** Advanced-config key holding the BCP-47 UI language tag. */
public static final String KEY = "ui.language";

// getBundle(base, locale) falls back to the default-locale bundle before the base bundle. A no-fallback control
// keeps the chain messages_<chosen> -> messages (English) only, so a missing key never leaks the OS-locale text.
// The Control overloads are unusable from a *named* module; JSignPdf ships on the classpath (jpackage images
// included), so this is fine.
private static final ResourceBundle.Control NO_FALLBACK =
ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_PROPERTIES);

private static final Locale SYSTEM_DEFAULT = Locale.getDefault(Locale.Category.DISPLAY);

private static volatile Locale current = SYSTEM_DEFAULT;

private UiLocale() {
}

/**
* Resolves and installs the UI locale. Reads {@code -o ui.language=<tag>} from the raw argv first (so
* {@code --help} is consistent), then the advanced configuration. An empty, missing, unparseable or unsupported
* value leaves the system default in place.
*/
public static void init(String[] args) {
String tag = scanArgs(args);
if (tag == null) {
tag = AppConfig.uiLanguage();
}
Locale locale = resolve(tag);
if (locale == null) {
current = SYSTEM_DEFAULT;
return;
}
current = locale;
// setDefault(Locale) sets the base default and both categories; pin FORMAT back to the OS locale afterwards
// so number/date formatting - and therefore the signed output - is untouched by a UI-language choice.
final Locale osFormat = Locale.getDefault(Locale.Category.FORMAT);
Locale.setDefault(locale);
Locale.setDefault(Locale.Category.FORMAT, osFormat);
ResourceBundle.clearCache();
Constants.RES.reload(bundle());
}

/** The resolved UI locale (the system default when unset). */
public static Locale current() {
return current;
}

/** The OS display locale, unaffected by an installed {@code ui.language} choice. */
public static Locale systemDefault() {
return SYSTEM_DEFAULT;
}

/** A {@link ResourceBundle} for {@link #current()} with English-only fallback (no OS-locale fallback). */
public static ResourceBundle bundle() {
return bundle(current);
}

/** A {@link ResourceBundle} for the given locale with English-only fallback (no OS-locale fallback). */
public static ResourceBundle bundle(Locale locale) {
return ResourceBundle.getBundle(Constants.RESOURCE_BUNDLE_BASE, locale, NO_FALLBACK);
}

/**
* Maps a configured tag to the matching entry of {@link SupportedLanguages#tags()}, or {@code ""} when it resolves
* to no bundled translation. Lets the Preferences selector show a hand-edited {@code zh_CN} or {@code de-AT} as the
* {@code zh-CN} / {@code de} item it actually loads, instead of an unselected entry.
*/
public static String canonicalTag(String tag) {
Locale locale = resolve(tag);
if (locale == null) {
return "";
}
String exact = locale.toLanguageTag();
return SupportedLanguages.tags().contains(exact) ? exact : locale.getLanguage();
}

/**
* Resolves a configured tag to a supported {@link Locale}, or {@code null} to keep the system default. Underscores
* are normalised to hyphens (so a hand-edited {@code zh_CN} works), the legacy Norwegian macrolanguage code
* {@code no} is mapped to {@code nb} (Java does not do this itself), and a region-less {@code zh} is expanded to
* {@code zh-CN} — the CLDR likely-subtags expansion, and the only way a bare {@code zh} can pick one of the two
* Chinese bundles rather than falling back to English.
*/
static Locale resolve(String tag) {
if (tag == null) {
return null;
}
String normalized = tag.trim().replace('_', '-');
if (normalized.isEmpty()) {
return null;
}
if (normalized.equalsIgnoreCase("no")) {
normalized = "nb";
} else if (normalized.regionMatches(true, 0, "no-", 0, 3)) {
normalized = "nb" + normalized.substring(2);
} else if (normalized.equalsIgnoreCase("zh")) {
normalized = "zh-CN";
}
Locale locale = Locale.forLanguageTag(normalized);
if (locale.getLanguage().isEmpty()) {
Constants.LOGGER.log(Level.INFO, "Ignoring unparseable ui.language=" + tag);
return null;
}
if (!SupportedLanguages.isSupported(locale)) {
Constants.LOGGER.log(Level.INFO, "Ignoring unsupported ui.language=" + tag);
return null;
}
return locale;
}

/**
* Pre-scans raw argv for {@code -o ui.language=<tag>} / {@code --option ui.language=<tag>} (also the attached
* {@code --option=...} form). A deliberate, tiny duplicate of the commons-cli parse in {@code SignerOptionsFromCmdLine},
* whose class init already needs the final locale. Returns the last matching tag, or {@code null}.
*/
private static String scanArgs(String[] args) {
if (args == null) {
return null;
}
String result = null;
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (arg == null) {
continue;
}
String value = null;
if (("-" + Constants.ARG_OPTION).equals(arg) || ("--" + Constants.ARG_OPTION_LONG).equals(arg)) {
if (i + 1 < args.length) {
value = args[i + 1];
}
} else if (arg.startsWith("--" + Constants.ARG_OPTION_LONG + "=")) {
value = arg.substring(("--" + Constants.ARG_OPTION_LONG + "=").length());
} else if (arg.startsWith("-" + Constants.ARG_OPTION)) {
value = arg.substring(("-" + Constants.ARG_OPTION).length());
}
String tag = tagOf(value);
if (tag != null) {
result = tag;
}
}
return result;
}

/** Extracts the tag from a {@code ui.language=<tag>} token, or {@code null} if it is not that key. */
private static String tagOf(String value) {
if (value == null) {
return null;
}
String prefix = KEY + "=";
return value.startsWith(prefix) ? value.substring(prefix.length()) : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ engine=openpdf
# regardless of this setting.
debug=false

# BCP-47 language tag of the user interface (e.g. de, fr, pt, zh-CN). Empty (the
# default) follows the operating-system locale. Read once at startup; restart
# JSignPdf to apply a change. The GUI Preferences dialog writes this key; on the
# CLI use -o ui.language=<tag>.
ui.language=

# Engine-specific configuration. Each engine module reads its own keys under the
# engine.<id>.* prefix (via EngineConfig). No keys are defined for the bundled
# OpenPDF engine
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,9 @@ jfx.gui.preferences.button.resetSection=Reset section to defaults
jfx.gui.preferences.restartHint=Takes effect after restart
jfx.gui.preferences.restartHint.tooltip=This setting is read once at startup. Restart JSignPdf for the change to take effect.
jfx.gui.preferences.section.general=General
jfx.gui.preferences.general.language=Language
jfx.gui.preferences.general.language.help=Language of the JSignPdf user interface. "System default" follows your operating-system settings.
jfx.gui.preferences.general.language.system=System default ({0})
jfx.gui.preferences.engine.help=Signing engine used to sign documents. OpenPDF produces standard PDF signatures; DSS produces PAdES baseline signatures (B/T/LT/LTA). The selection applies immediately and is the default for both the GUI and the CLI.
jfx.gui.preferences.general.debug=Debug output
jfx.gui.preferences.general.debug.help=Log verbose diagnostics during signing: the signing certificate chain, the loaded trust anchors, and every timestamp / AIA / CRL / OCSP network call. Enable this to investigate a signing failure. Applies immediately to both the GUI and the CLI.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,9 @@ jfx.gui.preferences.button.resetSection=Obnovit sekci na výchozí
jfx.gui.preferences.restartHint=Projeví se po restartu
jfx.gui.preferences.restartHint.tooltip=Toto nastavení se načítá pouze při spuštění. Restartujte JSignPdf, aby se změna projevila.
jfx.gui.preferences.section.general=Obecné
jfx.gui.preferences.general.language=Jazyk
jfx.gui.preferences.general.language.help=Jazyk uživatelského rozhraní JSignPdf. Volba „Výchozí jazyk systému“ se řídí nastavením operačního systému.
jfx.gui.preferences.general.language.system=Výchozí jazyk systému ({0})
jfx.gui.preferences.engine.help=Podpisový engine použitý k podepisování dokumentů. OpenPDF vytváří standardní PDF podpisy; DSS vytváří podpisy PAdES (B/T/LT/LTA). Výběr se projeví okamžitě a je výchozí pro GUI i CLI.
jfx.gui.preferences.general.debug=Ladicí výstup
jfx.gui.preferences.general.debug.help=Zaznamenávat podrobnou diagnostiku během podpisu: řetězec podpisového certifikátu, načtené kotvy důvěry a každé síťové volání časového razítka / AIA / CRL / OCSP. Zapněte pro řešení selhání podpisu. Projeví se okamžitě pro GUI i CLI.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,9 @@ jfx.gui.preferences.button.resetSection=Abschnitt auf Standardwerte zurücksetze
jfx.gui.preferences.restartHint=Wird nach Neustart wirksam
jfx.gui.preferences.restartHint.tooltip=Diese Einstellung wird einmal beim Start gelesen. Starten Sie JSignPdf neu, damit die Änderung wirksam wird.
jfx.gui.preferences.section.general=Allgemein
jfx.gui.preferences.general.language=Sprache
jfx.gui.preferences.general.language.help=Sprache der JSignPdf-Benutzeroberfläche. „Systemvorgabe“ folgt den Einstellungen des Betriebssystems.
jfx.gui.preferences.general.language.system=Systemvorgabe ({0})
jfx.gui.preferences.engine.help=Signatur-Engine, die zum Signieren von Dokumenten verwendet wird. OpenPDF erzeugt Standard-PDF-Signaturen; DSS erzeugt PAdES-Baseline-Signaturen (B/T/LT/LTA). Die Auswahl wird sofort wirksam und ist die Vorgabe für die Oberfläche und die Kommandozeile.
jfx.gui.preferences.general.debug=Debug-Ausgabe
jfx.gui.preferences.general.debug.help=Ausführliche Diagnose während des Signierens protokollieren: die Zertifikatskette des Unterzeichners, die geladenen Vertrauensanker und jeden Netzwerkaufruf für Zeitstempel / AIA / CRL / OCSP. Aktivieren Sie dies, um einen Signaturfehler zu untersuchen. Wird sofort für die Oberfläche und die Kommandozeile wirksam.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,9 @@ jfx.gui.preferences.button.resetSection=Επαναφορά ενότητας στ
jfx.gui.preferences.restartHint=Ισχύει μετά την επανεκκίνηση
jfx.gui.preferences.restartHint.tooltip=Αυτή η ρύθμιση διαβάζεται μία φορά κατά την εκκίνηση. Επανεκκινήστε το JSignPdf για να εφαρμοστεί η αλλαγή.
jfx.gui.preferences.section.general=Γενικά
jfx.gui.preferences.general.language=Γλώσσα
jfx.gui.preferences.general.language.help=Γλώσσα του περιβάλλοντος χρήστη του JSignPdf. Η επιλογή «Προεπιλογή συστήματος» ακολουθεί τις ρυθμίσεις του λειτουργικού συστήματος.
jfx.gui.preferences.general.language.system=Προεπιλογή συστήματος ({0})
jfx.gui.preferences.engine.help=Η μηχανή υπογραφής που χρησιμοποιείται για την υπογραφή εγγράφων. Το OpenPDF παράγει τυπικές υπογραφές PDF· το DSS παράγει βασικές υπογραφές PAdES (B/T/LT/LTA). Η επιλογή εφαρμόζεται άμεσα και αποτελεί την προεπιλογή τόσο για το γραφικό περιβάλλον όσο και για τη γραμμή εντολών.
jfx.gui.preferences.general.debug=Έξοδος αποσφαλμάτωσης
jfx.gui.preferences.general.debug.help=Καταγραφή αναλυτικών διαγνωστικών κατά την υπογραφή: την αλυσίδα πιστοποιητικών υπογραφής, τις φορτωμένες άγκυρες εμπιστοσύνης και κάθε δικτυακή κλήση χρονοσήμανσης / AIA / CRL / OCSP. Ενεργοποιήστε το για τη διερεύνηση μιας αποτυχίας υπογραφής. Εφαρμόζεται άμεσα τόσο στο γραφικό περιβάλλον όσο και στη γραμμή εντολών.
Expand Down
Loading
Loading