diff --git a/design-doc/3.2-language-selector.md b/design-doc/3.2-language-selector.md new file mode 100644 index 00000000..84255ecc --- /dev/null +++ b/design-doc/3.2-language-selector.md @@ -0,0 +1,319 @@ +# 3.2 — UI language selector (`ui.language`) + +Issue: [#444](https://github.com/intoolswetrust/jsignpdf/issues/444) — milestone 3.2.0. + +## Goal + +Let the user pick the interface language explicitly instead of always following the OS locale. Motivating +report: a Portuguese-locale desktop made error messages hard to match against the English documentation. + +Requirements from the issue: + +1. Language selector in the JavaFX Preferences dialog, listing the bundled translations. +2. Choice persisted in the advanced configuration, overriding the auto-detected locale at startup. +3. "System default" stays the default → no behaviour change for existing users. +4. GUI and CLI agree on the same setting. + +## Current state + +- Bundles: `engines/api/src/main/resources/net/sf/jsignpdf/translations/messages*.properties`, base name + `net.sf.jsignpdf.translations.messages` (`Constants.RESOURCE_BUNDLE_BASE`). +- `Constants.RES` is a `public static final ResourceProvider` built during `Constants` class init from + `ResourceBundle.getBundle(RESOURCE_BUNDLE_BASE)` — i.e. from the JVM default locale, at whatever moment + `Constants` first loads. Everything (CLI, Swing, `FxResourceProvider`) reads through it. +- The JavaFX side additionally calls `ResourceBundle.getBundle(base)` twice for FXML `%key` resolution: + `JSignPdfApp.start()` and `PreferencesController.show()`. +- `AdvancedConfig` / `AppConfig` already provide the layered store (`-o` overrides → user + `advanced.properties` → bundled `advanced.default.properties`), so the CLI half of requirement 4 comes for + free once the key exists (`-o ui.language=de`). +- Preferences is a `TabPane` (`Preferences.fxml` + `PreferencesController` + `PreferencesViewModel`), with an + established `jfx.gui.preferences.restartHint` label style for startup-only settings. + +## Decisions + +| Question | Decision | +|---|---| +| When does a change take effect? | **On restart.** Store the key, show the existing restart hint. No FXML reload / ViewModel rebuild. | +| How far does the locale reach? | **Base default + `Category.DISPLAY`, with `Category.FORMAT` pinned to the OS locale.** UI text follows the choice; number/date formatting (visible-signature L2 text, logs) keeps following the OS locale, so a UI preference can never change signed output. The base default has to move as well because `ResourceBundle.getBundle(baseName)` — how JavaFX loads its own control strings — resolves against `Locale.getDefault()` and ignores the DISPLAY category; see [Reach of the install](#reach-of-the-install). | +| Where does the language list come from? | **Static list in code + a guard test** that scans the translations directory and fails when the two diverge. No build-time generation, no runtime classpath scanning (jar / jlink / jpackage safe). | +| `messages_nb-NO.properties` | **Renamed** to `messages_nb.properties` as part of this work (see below). | + +## Config key + +New key in `advanced.properties` (and documented in `advanced.default.properties`): + +```properties +# BCP-47 language tag of the user interface, e.g. de, fr, pt, zh-CN. +# Empty (default) -> follow the operating-system locale. +# Read once at startup; restart JSignPdf to apply a change. +ui.language= +``` + +- Value format: BCP-47 tag, parsed with `Locale.forLanguageTag`. Underscores are normalised to hyphens first + so a hand-edited `zh_CN` also works, and the legacy Norwegian code `no` is mapped to `nb` (Java does not do + this itself, and some systems still report `no_NO`). +- Empty / missing / unparseable / not-in-the-list → system default, logged at `INFO` for the unparseable + case, never fatal. +- Accessor: `AppConfig.uiLanguage()` returning the raw tag (`null` when unset), next to the other accessors. + +## Startup wiring + +The only real constraint: `Constants.RES` and the commons-cli `OPTS` descriptions in +`SignerOptionsFromCmdLine` capture strings during class initialisation, so the locale has to be settled +before those classes load — otherwise `-o ui.language=de --help` would print half-German help. + +New helper `net.sf.jsignpdf.utils.UiLocale` in `engines/api`: + +```java +/** Resolves the configured UI locale and installs it. Call once, first thing in main(). */ +public static void init(String[] args); // argv pre-scan, then advanced config +public static Locale current(); // resolved locale (system default when unset) +``` + +`init(String[] args)`: + +1. Pre-scan raw argv for `-o ui.language=` / `--option ui.language=`. This is a deliberate + duplicate of the later `-o` parsing: the real parse happens inside `SignerOptionsFromCmdLine`, whose + class init already needs the final locale. ~15 lines, no commons-cli involvement. +2. Otherwise read `AppConfig.uiLanguage()` (this touches `PropertyStoreFactory` → config-dir resolution and + legacy migration, which would happen a few lines later anyway). +3. Resolve to a `Locale`; if it is not in the supported list, keep the system default. +4. Install: save `Locale.getDefault(Category.FORMAT)`, call `Locale.setDefault(locale)` (which moves the base + default *and* both categories), then put `Category.FORMAT` back to the saved OS value. Finally + `ResourceBundle.clearCache()` and `Constants.RES.reload(UiLocale.bundle())`. + +`ResourceProvider` gets a `reload(ResourceBundle)` that swaps its (already non-`final`, now `volatile`) +`bundle` field. It takes the bundle rather than a `Locale` so the base name and the fallback policy stay owned +by `UiLocale` alone, and `utils.ResourceProvider` does not have to depend on `Constants` — which constructs a +`ResourceProvider` itself. Keeping `Constants.RES` as the single shared instance avoids touching the ~200 +static-import call sites, and the reload makes the whole thing order-independent: even if something loaded +`Constants` earlier, the bundle is corrected before any string is read. + +### Reach of the install + +`Locale.setDefault(Category, …)` writes only that category's field; `Locale.getDefault()` keeps returning the +OS locale. That matters because `ResourceBundle.getBundle(baseName)` — the no-explicit-locale overload — +resolves against `Locale.getDefault()`, **not** against `Category.DISPLAY`. JavaFX's own control strings come +from exactly that overload: + +```java +// com/sun/javafx/scene/control/skin/resources/ControlResources.java +return ResourceBundle.getBundle(BASE_NAME).getString(key); +``` + +`ButtonType.OK` / `ButtonType.CANCEL` resolve their labels lazily through it, and the app uses them widely +(`MainWindowController`, `ManagePresetsDialog`, ~8 `new Alert(...)` sites). A DISPLAY-only install would +therefore give a Portuguese-locale user who picks German a German app with *Cancelar* buttons — a +half-translated UI in exactly the situation issue #444 is about. Hence the base default moves too, with +`Category.FORMAT` explicitly pinned back to the OS locale. + +The cost of moving the base default is that `String.toLowerCase()` / `toUpperCase()` without an explicit +locale now follow the UI language. All 14 such call sites in the codebase compare ASCII literals (`".pdf"`, +`"linux"`, hex digits), and the Turkish-I hazard cannot trigger anyway: `tr`, `az` and `lt` are not among the +bundled translations, and none of those literals contains an `i`/`I`. + +Call sites: + +- `Signer.main(String[] args)` — `UiLocale.init(args);` as the first statement, before + `new SignerOptionsFromCmdLine()`. +- `JSignPdfApp.start()` and `PreferencesController.show()` — pass the locale explicitly: + `ResourceBundle.getBundle(Constants.RESOURCE_BUNDLE_BASE, UiLocale.current(), NO_FALLBACK)`. +- Swing (`SignPdfForm`) needs no change: it reads `Constants.RES` at construction, i.e. after `init`. + +### Bundle fallback detail + +`getBundle(base, locale)` falls back to the *default-locale* bundle before the base bundle. On a Portuguese +system a user choosing Slovak would get Portuguese for any key missing from `messages_sk.properties`. Use +`ResourceBundle.Control.getNoFallbackControl(FORMAT_PROPERTIES)` so the chain is +`messages_` → `messages` (English) only. (The `Control` overloads are unusable from a *named* module; +JSignPdf ships on the classpath, including inside the jpackage images, so this is fine — worth a comment at +the call site.) + +## Supported-language list + +`net.sf.jsignpdf.utils.SupportedLanguages` in `engines/api`: + +```java +/** BCP-47 tags of the bundled translations, English (the base bundle) first. */ +public static List tags(); +/** Label for the selector: native name + English name, e.g. "Deutsch (German)". */ +public static String displayName(Locale locale); +``` + +Current inventory (19 entries): `en` (base bundle), `cs`, `de`, `el`, `es`, `fr`, `hr`, `hu`, `hy`, `it`, +`ja`, `nb`, `pl`, `pt`, `ru`, `sk`, `ta`, `zh-CN`, `zh-TW`. + +Plain language codes are preferred over language+region; the region subtag is kept only where it actually +disambiguates. `zh-CN` / `zh-TW` stay because there the region stands in for Simplified vs Traditional +script. `pt` (not `pt-PT` / `pt-BR`) and now `nb` follow the general rule and let the `ResourceBundle` +fallback chain cover the regional variants. + +Labels come from `Locale.getDisplayName(locale)` (self-name) plus `getDisplayName(Locale.ENGLISH)`, so the +list stays readable to someone who cannot read the current UI language — the exact scenario in the issue. +`getDisplayName` rather than `getDisplayLanguage` because the latter renders `zh-CN` and `zh-TW` identically +as "中文". The region it adds is already bracketed, so a name containing `(` switches the separator to an +en dash — `中文 (中国) – Chinese (China)`, not a nested `中文 (中国) (Chinese (China))`. + +The selector keeps `tags()` order (English first, then alphabetical by tag), with "System default" pinned +above it. Sorting by native name was considered and dropped: it puts the list in an order that no user can +predict, since the sort key is written in a script they may not read. + +Guard test `SupportedLanguagesTest`: lists `messages*.properties` under the translations directory, maps file +names to tags, and asserts set equality with `tags()`. When Weblate adds a language, CI fails with a message +naming the missing tag. + +## Norwegian bundle fix + +`messages_nb-NO.properties` contains a real, complete Norwegian translation but **is never loaded**: +`ResourceBundle` builds candidate names with underscores (`messages_nb_NO.properties`), so the hyphenated +file is dead weight today. Without the fix the selector would list Norwegian and silently render English. + +The rename target is `messages_nb.properties`, not `messages_nb_NO.properties`: + +- **Wider match.** The candidate chain for an `nb_NO` user is `messages_nb_NO` → `messages_nb` → `messages`, + so a file named `messages_nb` serves both `nb` and `nb-NO`, while `messages_nb_NO` would leave a plain-`nb` + user on English. +- **The region subtag adds nothing.** `nb` *is* Bokmål — CLDR's likely-subtags expansion is `nb` → + `nb-Latn-NO` — so `nb-NO` is redundant under BCP-47. Contrast `zh-CN`/`zh-TW`, where the region genuinely + disambiguates Simplified vs Traditional and therefore stays. +- **Consistency.** The repo already ships generic `messages_pt.properties` rather than `pt_PT`/`pt_BR`. + +In scope here: `git mv messages_nb-NO.properties messages_nb.properties`. **That is the whole fix — no +Weblate configuration change is required.** Verified against the live component +(`GET /api/components/jsignpdf/messages/` and its `translations/` listing, both readable anonymously): + +- The Weblate language for this translation is **`nb_NO`**, and its built-in alias list already contains + `nb` (`nb, nb_nb, no, no_nb, no_no, nob, …`). After the rename Weblate parses the file code `nb`, resolves + it through that alias to the same language object, and keeps the existing 100 %-translated content — it + just starts writing to the new filename. +- `filemask` is `…/messages_*.properties` and `language_regex` is `^[^.]+$`; `nb` matches both. +- `language_code_style` is `''` (file-format default, which is underscore-based for `properties`). The + hyphenated `nb-NO` therefore was **not** Weblate-generated — it was hand-added to the repo and merely + adopted by the filemask. Nothing in the component config will recreate it. +- There is no `language_aliases` field in this component's API representation at all, so the alias setting + is neither needed nor available. + +The same listing confirms the `zh` decision above: Weblate tracks those as `zh_Hans` / `zh_Hant` while the +files stay `messages_zh_CN` / `messages_zh_TW`, which is what Java's `ResourceBundle` needs. Leave them. + +`SupportedLanguages` maps tag `nb` → file `messages_nb.properties`; the guard test's file→tag mapping +converts `_` to `-`, which also keeps `zh_CN`/`zh_TW` honest. + +After the rename lands on `master`, Weblate picks it up on its next repository poll (or immediately via the +GitHub hook); worth a glance at the component afterwards to confirm Norwegian still reads 100 %. + +## Preferences UI + +The selector goes into the existing **General** tab (`tabGeneral`), **not** a new tab. That tab currently +holds the signing-engine `ChoiceBox` and the debug checkbox; the language control is added as its **first** +row, above the engine selector: + +``` +General + Language [ System default (Português) v ] + Language of the JSignPdf user interface. "System default" follows + your operating-system settings. + ⟳ Takes effect after restart + + Signing engine [ OpenPDF v ] + Signing engine used to sign documents. … + + ☐ Debug output + Log verbose diagnostics during signing … +``` + +Placing it first, above the immediate-effect engine/debug controls, keeps it findable and — since the whole +point of the feature is to escape a UI you can't read — puts the one control that fixes that at the top. + +- `ChoiceBox` (`cmbUiLanguage`) holding tags, with a `StringConverter` rendering + `SupportedLanguages.displayName`; the first item is the empty tag rendered as + `jfx.gui.preferences.general.language.system` = `System default ({0})`, filled with the display name of + `UiLocale.systemDefault()` — the OS locale as captured before `init()` installs a choice, not + `Locale.getDefault(DISPLAY)`, which by then already follows the configured language. +- Reuses the existing `pref-restart-hint` label + tooltip. Note this is the General tab's **only** + restart-scoped control (engine and debug both apply immediately), so the hint sits on the language row + itself, not the tab. +- `PreferencesViewModel`: `StringProperty uiLanguage`, wired into `loadFrom` (`cfg.getProperty("ui.language")`), + `writeTo` (`writeStringOrRemove` → empty removes the key), and folded into the General tab's existing + reset-to-defaults path so "Reset section to defaults" clears it like the other General controls. + +Live re-rendering of the running UI is deliberately out of scope (see Decisions); it would mean reloading +`MainWindow.fxml`, re-creating every controller and ViewModel and restoring document/preview/placement +state, for a setting users change roughly once. + +## CLI + +No new flag. The generic advanced-override mechanism already covers it: + +``` +jsignpdf --help -o ui.language=de +jsignpdf doc.pdf -o ui.language=en ... +``` + +The argv pre-scan in `UiLocale.init` is what makes `--help` fully consistent. Document the form in the guide +next to the other `-o` examples; requirement 4 is then just "the GUI writes the key the CLI already reads". + +## New translation keys + +Added to the canonical English bundle only (other locales come via Weblate): + +Keys live under the existing `jfx.gui.preferences.general.*` namespace (the `section.general` key already +exists): + +```properties +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}) +``` + +No `hlp.uiLanguage` line: `-o` is a generic mechanism and `--help` documents it once, so a per-key help entry +would set a precedent for every advanced key. The guide covers the form instead. + +## Testing + +- `SupportedLanguagesTest` — list vs. translation files (guard, described above); both `nb` and `nb-NO` + resolve to a bundle whose strings are actually Norwegian (regression test for the rename, and proof that + the plain-language file covers the regional locale). +- `UiLocaleTest` — tag parsing (`de`, `zh-CN`, `zh_CN`, `nb-NO`, legacy `no` → `nb`, bare `zh` → `zh-CN`); + unknown tag → system default; empty → system default; garbage → system default + no exception; + `canonicalTag` mapping to a selector item; after `init`, base default and `Category.DISPLAY` follow the + choice while `Category.FORMAT` is untouched. Save/restore the JVM default locale **and `Constants.RES`** + around each case — `init` swaps the process-wide provider, which would otherwise leak a translated bundle + into whatever test runs next in the same surefire fork. +- `PreferencesViewModel` round-trip — `loadFrom`/`writeTo` for the new property, empty value removes the key, + `applyDefaults` resets it. +- `FxTranslationsTest` — extend `TEST_LOCALES` with `nb_NO`, and add `Preferences.fxml` to the loaded views so + the new General-tab language `%keys` are covered. + +## Docs + +- `website/docs/JSignPdf.adoc` — `advanced.properties` key table (`ui.language`), a short Preferences + subsection, and the `-o ui.language=` example. +- `engines/api/.../conf/advanced.default.properties` — the commented key block above. +- `sample.properties` — mention alongside the other advanced keys if it lists them. +- `distribution/doc/release-notes/3.2.0.md` — one bullet for the selector, one for the Norwegian fix. + +## Touch list + +| File | Change | +|---|---| +| `engines/api/.../utils/UiLocale.java` | **new** — resolve + install the UI locale | +| `engines/api/.../utils/SupportedLanguages.java` | **new** — tag list + display names | +| `engines/api/.../utils/ResourceProvider.java` | `reload(ResourceBundle)`, `bundle` field made `volatile` | +| `engines/api/.../utils/AppConfig.java` | `uiLanguage()` | +| `engines/api/.../conf/advanced.default.properties` | documented `ui.language=` | +| `engines/api/.../translations/messages.properties` | new `jfx.gui.preferences.general.language*` keys | +| `engines/api/.../translations/messages_nb-NO.properties` | renamed to `messages_nb.properties` | +| `jsignpdf/.../Signer.java` | `UiLocale.init(args)` first in `main` | +| `jsignpdf/.../fx/JSignPdfApp.java`, `fx/preferences/PreferencesController.java` | explicit locale + no-fallback control in `getBundle`; `cmbUiLanguage` wiring | +| `jsignpdf/.../fx/preferences/PreferencesViewModel.java` | `uiLanguage` property, load/write/defaults | +| `jsignpdf/src/main/resources/.../view/Preferences.fxml` | language row added to the existing General tab | +| tests + docs | as listed above | + +## Out of scope + +- Live language switching without restart. +- A dedicated CLI flag (e.g. `--language`) — `-o ui.language=` covers it without widening the CLI surface. +- Per-document or per-preset language settings. +- Localising the visible-signature L2 text or output-file suffix (`output.suffix` is already a separate, + user-settable advanced key). diff --git a/distribution/doc/release-notes/3.2.0.md b/distribution/doc/release-notes/3.2.0.md index 6c9cd588..ed917cc5 100644 --- a/distribution/doc/release-notes/3.2.0.md +++ b/distribution/doc/release-notes/3.2.0.md @@ -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. diff --git a/engines/api/src/main/java/net/sf/jsignpdf/utils/AppConfig.java b/engines/api/src/main/java/net/sf/jsignpdf/utils/AppConfig.java index ce6aa9a6..566ddefd 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/utils/AppConfig.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/utils/AppConfig.java @@ -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 / diff --git a/engines/api/src/main/java/net/sf/jsignpdf/utils/ResourceProvider.java b/engines/api/src/main/java/net/sf/jsignpdf/utils/ResourceProvider.java index bef10742..96ca7748 100644 --- a/engines/api/src/main/java/net/sf/jsignpdf/utils/ResourceProvider.java +++ b/engines/api/src/main/java/net/sf/jsignpdf/utils/ResourceProvider.java @@ -24,7 +24,7 @@ */ public class ResourceProvider { - private ResourceBundle bundle; + private volatile ResourceBundle bundle; /** * Constructor which takes a not-null {@link ResourceBundle} as an argument. @@ -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-null + */ + 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 * diff --git a/engines/api/src/main/java/net/sf/jsignpdf/utils/SupportedLanguages.java b/engines/api/src/main/java/net/sf/jsignpdf/utils/SupportedLanguages.java new file mode 100644 index 00000000..2513828b --- /dev/null +++ b/engines/api/src/main/java/net/sf/jsignpdf/utils/SupportedLanguages.java @@ -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 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 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 + ")"; + } +} diff --git a/engines/api/src/main/java/net/sf/jsignpdf/utils/UiLocale.java b/engines/api/src/main/java/net/sf/jsignpdf/utils/UiLocale.java new file mode 100644 index 00000000..c558867a --- /dev/null +++ b/engines/api/src/main/java/net/sf/jsignpdf/utils/UiLocale.java @@ -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. + * + *

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_ -> 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=} 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=} / {@code --option ui.language=} (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=} 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; + } +} diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/conf/advanced.default.properties b/engines/api/src/main/resources/net/sf/jsignpdf/conf/advanced.default.properties index eee4942c..ae098cec 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/conf/advanced.default.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/conf/advanced.default.properties @@ -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=. +ui.language= + # Engine-specific configuration. Each engine module reads its own keys under the # engine..* prefix (via EngineConfig). No keys are defined for the bundled # OpenPDF engine diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties index 3c45c5cb..b864f44f 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages.properties @@ -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. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_cs.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_cs.properties index e5f48bb6..f12717ba 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_cs.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_cs.properties @@ -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. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_de.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_de.properties index b2495216..e1f0a3df 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_de.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_de.properties @@ -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. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_el.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_el.properties index bf093e5a..c88bd982 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_el.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_el.properties @@ -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. Ενεργοποιήστε το για τη διερεύνηση μιας αποτυχίας υπογραφής. Εφαρμόζεται άμεσα τόσο στο γραφικό περιβάλλον όσο και στη γραμμή εντολών. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_es.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_es.properties index dcc52bd2..5bb73d2c 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_es.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_es.properties @@ -505,6 +505,9 @@ jfx.gui.preferences.button.resetSection=Restablecer sección a valores predeterm jfx.gui.preferences.restartHint=Surte efecto tras reiniciar jfx.gui.preferences.restartHint.tooltip=Esta configuración se lee una vez al inicio. Reinicie JSignPdf para que el cambio surta efecto. jfx.gui.preferences.section.general=General +jfx.gui.preferences.general.language=Idioma +jfx.gui.preferences.general.language.help=Idioma de la interfaz de usuario de JSignPdf. La opción «Predeterminado del sistema» sigue la configuración del sistema operativo. +jfx.gui.preferences.general.language.system=Predeterminado del sistema ({0}) jfx.gui.preferences.engine.help=Motor de firma usado para firmar documentos. OpenPDF produce firmas PDF estándar; DSS produce firmas PAdES base (B/T/LT/LTA). La selección se aplica de inmediato y es la predeterminada tanto para la interfaz gráfica como para la línea de comandos. jfx.gui.preferences.general.debug=Salida de depuración jfx.gui.preferences.general.debug.help=Registrar diagnósticos detallados durante la firma: la cadena de certificados de firma, los anclajes de confianza cargados y cada llamada de red de sello de tiempo / AIA / CRL / OCSP. Actívelo para investigar un fallo de firma. Se aplica de inmediato tanto a la interfaz gráfica como a la línea de comandos. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_fr.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_fr.properties index eb96d334..d69aacaf 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_fr.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_fr.properties @@ -504,6 +504,9 @@ jfx.gui.preferences.button.resetSection=Réinitialiser la section aux valeurs pa jfx.gui.preferences.restartHint=Prend effet après le redémarrage jfx.gui.preferences.restartHint.tooltip=Ce paramètre est lu une seule fois au démarrage. Redémarrez JSignPdf pour que la modification prenne effet. jfx.gui.preferences.section.general=Général +jfx.gui.preferences.general.language=Langue +jfx.gui.preferences.general.language.help=Langue de l'interface utilisateur de JSignPdf. L'option « Valeur par défaut du système » suit les paramètres du système d'exploitation. +jfx.gui.preferences.general.language.system=Valeur par défaut du système ({0}) jfx.gui.preferences.engine.help=Moteur de signature utilisé pour signer les documents. OpenPDF produit des signatures PDF standard ; DSS produit des signatures PAdES de base (B/T/LT/LTA). La sélection s'applique immédiatement et constitue la valeur par défaut pour l'interface graphique et la ligne de commande. jfx.gui.preferences.general.debug=Sortie de débogage jfx.gui.preferences.general.debug.help=Journaliser des diagnostics détaillés pendant la signature : la chaîne de certificats de signature, les ancres de confiance chargées et chaque appel réseau d'horodatage / AIA / CRL / OCSP. Activez cette option pour analyser un échec de signature. S'applique immédiatement à l'interface graphique et à la ligne de commande. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hr.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hr.properties index 385d46c6..6d113cda 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hr.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hr.properties @@ -503,6 +503,9 @@ jfx.gui.preferences.button.resetSection=Vrati odjeljak na zadane vrijednosti jfx.gui.preferences.restartHint=Stupa na snagu nakon ponovnog pokretanja jfx.gui.preferences.restartHint.tooltip=Ova se postavka čita jednom pri pokretanju. Ponovno pokrenite JSignPdf da bi promjena stupila na snagu. jfx.gui.preferences.section.general=Općenito +jfx.gui.preferences.general.language=Jezik +jfx.gui.preferences.general.language.help=Jezik korisničkog sučelja programa JSignPdf. Opcija „Zadano prema sustavu” prati postavke operacijskog sustava. +jfx.gui.preferences.general.language.system=Zadano prema sustavu ({0}) jfx.gui.preferences.engine.help=Pogon za potpisivanje koji se upotrebljava za potpisivanje dokumenata. OpenPDF stvara standardne PDF potpise; DSS stvara osnovne PAdES potpise (B/T/LT/LTA). Odabir se primjenjuje odmah i zadan je i za grafičko sučelje i za naredbeni redak. jfx.gui.preferences.general.debug=Dijagnostički ispis jfx.gui.preferences.general.debug.help=Bilježi detaljnu dijagnostiku tijekom potpisivanja: lanac certifikata za potpisivanje, učitana sidra povjerenja te svaki mrežni poziv vremenskog žiga / AIA / CRL / OCSP. Uključite za istraživanje neuspjeha potpisivanja. Primjenjuje se odmah i na grafičko sučelje i na naredbeni redak. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hu.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hu.properties index 0d598d30..6081ca65 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hu.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hu.properties @@ -504,6 +504,9 @@ jfx.gui.preferences.button.resetSection=Szakasz visszaállítása alapértelmez jfx.gui.preferences.restartHint=Újraindítás után lép életbe jfx.gui.preferences.restartHint.tooltip=Ez a beállítás csak indításkor kerül beolvasásra. Indítsa újra a JSignPdf-et, hogy a változás életbe lépjen. jfx.gui.preferences.section.general=Általános +jfx.gui.preferences.general.language=Nyelv +jfx.gui.preferences.general.language.help=A JSignPdf felhasználói felületének nyelve. A „Rendszer szerinti” beállítás az operációs rendszer nyelvét követi. +jfx.gui.preferences.general.language.system=Rendszer szerinti ({0}) jfx.gui.preferences.engine.help=A dokumentumok aláírásához használt aláírómotor. Az OpenPDF szabványos PDF-aláírásokat készít; a DSS PAdES-alapaláírásokat (B/T/LT/LTA) készít. A kiválasztás azonnal érvénybe lép, és ez az alapértelmezés a grafikus felülethez és a parancssorhoz is. jfx.gui.preferences.general.debug=Hibakeresési kimenet jfx.gui.preferences.general.debug.help=Részletes diagnosztika naplózása aláírás közben: az aláíró tanúsítványlánc, a betöltött megbízhatósági horgonyok, valamint minden időbélyeg / AIA / CRL / OCSP hálózati hívás. Kapcsolja be aláírási hiba kivizsgálásához. Azonnal érvénybe lép a grafikus felületen és a parancssorban is. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hy.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hy.properties index 69d457fa..3dd92056 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hy.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_hy.properties @@ -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 յուրաքանչյուր ցանցային կանչ: Միացրեք սա ստորագրման ձախողումը հետազոտելու համար: Կիրառվում է անմիջապես ինչպես գրաֆիկական միջերեսի, այնպես էլ հրամանային տողի համար: diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_it.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_it.properties index 5f7fce26..3e0418fd 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_it.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_it.properties @@ -498,6 +498,9 @@ jfx.gui.preferences.button.resetSection=Ripristina sezione ai valori predefiniti jfx.gui.preferences.restartHint=Ha effetto dopo il riavvio jfx.gui.preferences.restartHint.tooltip=Questa impostazione viene letta una sola volta all'avvio. Riavvia JSignPdf per applicare la modifica. jfx.gui.preferences.section.general=Generale +jfx.gui.preferences.general.language=Lingua +jfx.gui.preferences.general.language.help=Lingua dell'interfaccia utente di JSignPdf. L'opzione «Predefinita di sistema» segue le impostazioni del sistema operativo. +jfx.gui.preferences.general.language.system=Predefinita di sistema ({0}) jfx.gui.preferences.engine.help=Motore di firma usato per firmare i documenti. OpenPDF produce firme PDF standard; DSS produce firme PAdES base (B/T/LT/LTA). La selezione si applica immediatamente ed è quella predefinita sia per l'interfaccia grafica sia per la riga di comando. jfx.gui.preferences.general.debug=Output di debug jfx.gui.preferences.general.debug.help=Registra diagnostica dettagliata durante la firma: la catena di certificati di firma, le ancore di fiducia caricate e ogni chiamata di rete per marca temporale / AIA / CRL / OCSP. Attivalo per analizzare un errore di firma. Si applica immediatamente sia all'interfaccia grafica sia alla riga di comando. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ja.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ja.properties index 370e5559..585c0b05 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ja.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ja.properties @@ -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)を生成します。選択は即座に適用され、GUI と CLI の両方の既定値になります。 jfx.gui.preferences.general.debug=デバッグ出力 jfx.gui.preferences.general.debug.help=署名中に詳細な診断情報を記録します: 署名証明書チェーン、読み込まれたトラストアンカー、タイムスタンプ / AIA / CRL / OCSP のすべてのネットワーク呼び出し。署名の失敗を調査するには有効にしてください。GUI と CLI の両方に即座に適用されます。 diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb-NO.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb.properties similarity index 99% rename from engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb-NO.properties rename to engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb.properties index 7802573a..844d98e4 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb-NO.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_nb.properties @@ -504,6 +504,9 @@ jfx.gui.preferences.button.resetSection=Tilbakestill seksjon til standardverdier jfx.gui.preferences.restartHint=Trer i kraft etter omstart jfx.gui.preferences.restartHint.tooltip=Denne innstillingen leses én gang ved oppstart. Start JSignPdf på nytt for at endringen skal tre i kraft. jfx.gui.preferences.section.general=Generelt +jfx.gui.preferences.general.language=Språk +jfx.gui.preferences.general.language.help=Språk for JSignPdf-brukergrensesnittet. «Systemstandard» følger innstillingene i operativsystemet. +jfx.gui.preferences.general.language.system=Systemstandard ({0}) jfx.gui.preferences.engine.help=Signeringsmotor som brukes til å signere dokumenter. OpenPDF produserer standard PDF-signaturer; DSS produserer PAdES-baselinesignaturer (B/T/LT/LTA). Valget trer i kraft umiddelbart og er standard for både det grafiske grensesnittet og kommandolinjen. jfx.gui.preferences.general.debug=Feilsøkingsutdata jfx.gui.preferences.general.debug.help=Logg detaljert diagnostikk under signering: signeringssertifikatkjeden, de innlastede tillitsankrene og hvert nettverkskall for tidsstempel / AIA / CRL / OCSP. Aktiver dette for å undersøke en signeringsfeil. Trer i kraft umiddelbart for både det grafiske grensesnittet og kommandolinjen. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pl.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pl.properties index 88970873..e02a9fdf 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pl.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pl.properties @@ -504,6 +504,9 @@ jfx.gui.preferences.button.resetSection=Przywróć sekcję do wartości domyśln jfx.gui.preferences.restartHint=Działa po ponownym uruchomieniu jfx.gui.preferences.restartHint.tooltip=To ustawienie jest odczytywane tylko przy starcie. Uruchom ponownie JSignPdf, aby zmiana zaczęła obowiązywać. jfx.gui.preferences.section.general=Ogólne +jfx.gui.preferences.general.language=Język +jfx.gui.preferences.general.language.help=Język interfejsu użytkownika JSignPdf. Opcja „Domyślny język systemu” jest zgodna z ustawieniami systemu operacyjnego. +jfx.gui.preferences.general.language.system=Domyślny język systemu ({0}) jfx.gui.preferences.engine.help=Silnik podpisu używany do podpisywania dokumentów. OpenPDF tworzy standardowe podpisy PDF; DSS tworzy podpisy PAdES (B/T/LT/LTA). Wybór jest stosowany natychmiast i stanowi wartość domyślną zarówno dla interfejsu graficznego, jak i wiersza poleceń. jfx.gui.preferences.general.debug=Dane diagnostyczne jfx.gui.preferences.general.debug.help=Rejestruj szczegółową diagnostykę podczas podpisywania: łańcuch certyfikatów podpisu, wczytane kotwice zaufania oraz każde wywołanie sieciowe znacznika czasu / AIA / CRL / OCSP. Włącz, aby zbadać niepowodzenie podpisywania. Stosowane natychmiast zarówno dla interfejsu graficznego, jak i wiersza poleceń. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pt.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pt.properties index a329bd21..8b9decda 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pt.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_pt.properties @@ -505,6 +505,9 @@ jfx.gui.preferences.button.resetSection=Restaurar secção para valores predefin jfx.gui.preferences.restartHint=Tem efeito após reinício jfx.gui.preferences.restartHint.tooltip=Esta definição é lida uma vez no arranque. Reinicie o JSignPdf para que a alteração tenha efeito. jfx.gui.preferences.section.general=Geral +jfx.gui.preferences.general.language=Idioma +jfx.gui.preferences.general.language.help=Idioma da interface de utilizador do JSignPdf. A opção «Predefinição do sistema» segue as configurações do sistema operativo. +jfx.gui.preferences.general.language.system=Predefinição do sistema ({0}) jfx.gui.preferences.engine.help=Motor de assinatura usado para assinar documentos. O OpenPDF produz assinaturas PDF padrão; o DSS produz assinaturas PAdES base (B/T/LT/LTA). A seleção aplica-se imediatamente e é a predefinição tanto para a interface gráfica como para a linha de comandos. jfx.gui.preferences.general.debug=Saída de depuração jfx.gui.preferences.general.debug.help=Registar diagnósticos detalhados durante a assinatura: a cadeia de certificados de assinatura, as âncoras de confiança carregadas e cada chamada de rede de marca temporal / AIA / CRL / OCSP. Ative para investigar uma falha de assinatura. Aplica-se imediatamente tanto à interface gráfica como à linha de comandos. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ru.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ru.properties index 27c382ff..e079b0ac 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ru.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ru.properties @@ -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. Включите для расследования сбоя подписания. Применяется немедленно как к графическому интерфейсу, так и к командной строке. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_sk.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_sk.properties index f424a74d..ec10b730 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_sk.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_sk.properties @@ -505,6 +505,9 @@ jfx.gui.preferences.button.resetSection=Obnoviť sekciu na predvolené hodnoty jfx.gui.preferences.restartHint=Prejaví sa po reštarte jfx.gui.preferences.restartHint.tooltip=Toto nastavenie sa načítava len pri spustení. Reštartujte JSignPdf, aby sa zmena prejavila. jfx.gui.preferences.section.general=Všeobecné +jfx.gui.preferences.general.language=Jazyk +jfx.gui.preferences.general.language.help=Jazyk používateľského rozhrania JSignPdf. Voľba „Predvolený jazyk systému“ sa riadi nastavením operačného systému. +jfx.gui.preferences.general.language.system=Predvolený jazyk systému ({0}) jfx.gui.preferences.engine.help=Podpisový engine použitý na podpisovanie dokumentov. OpenPDF vytvára štandardné PDF podpisy; DSS vytvára podpisy PAdES (B/T/LT/LTA). Výber sa prejaví okamžite a je predvolený pre GUI aj CLI. jfx.gui.preferences.general.debug=Ladiaci výstup jfx.gui.preferences.general.debug.help=Zaznamenávať podrobnú diagnostiku počas podpisovania: reťazec podpisového certifikátu, načítané kotvy dôvery a každé sieťové volanie časovej pečiatky / AIA / CRL / OCSP. Zapnite na riešenie zlyhania podpisu. Prejaví sa okamžite pre GUI aj CLI. diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ta.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ta.properties index f4292557..29b2fc4c 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ta.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_ta.properties @@ -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 நெட்வொர்க் அழைப்பு. கையொப்பத் தோல்வியை ஆராய இதை இயக்கவும். வரைகலை இடைமுகம் மற்றும் கட்டளை வரி ஆகிய இரண்டிற்கும் உடனடியாகப் பொருந்தும். diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_CN.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_CN.properties index 6f9c5149..80afc139 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_CN.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_CN.properties @@ -449,6 +449,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 网络调用。启用此项以排查签名失败。会立即应用于图形界面和命令行。 diff --git a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_TW.properties b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_TW.properties index 4390a593..b0ab8040 100644 --- a/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_TW.properties +++ b/engines/api/src/main/resources/net/sf/jsignpdf/translations/messages_zh_TW.properties @@ -449,6 +449,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 網路呼叫。啟用此項以排查簽署失敗。會立即套用於圖形介面與命令列。 diff --git a/engines/api/src/test/java/net/sf/jsignpdf/utils/SupportedLanguagesTest.java b/engines/api/src/test/java/net/sf/jsignpdf/utils/SupportedLanguagesTest.java new file mode 100644 index 00000000..4ba73602 --- /dev/null +++ b/engines/api/src/test/java/net/sf/jsignpdf/utils/SupportedLanguagesTest.java @@ -0,0 +1,71 @@ +package net.sf.jsignpdf.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.net.URL; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Locale; +import java.util.TreeSet; + +import org.junit.Test; + +/** + * Guards {@link SupportedLanguages#tags()} against the actual {@code messages*.properties} translation files: when + * Weblate adds (or someone removes) a language, this test fails naming the diverging tag. Also verifies that the + * renamed Norwegian bundle is really loaded for both {@code nb} and {@code nb-NO}. + */ +public class SupportedLanguagesTest { + + private static final String DEFAULT_BUNDLE = "/net/sf/jsignpdf/translations/messages.properties"; + + @Test + public void tagsMatchTranslationFiles() throws Exception { + TreeSet fromFiles = new TreeSet<>(tagsFromTranslationFiles()); + TreeSet fromCode = new TreeSet<>(SupportedLanguages.tags()); + assertEquals("SupportedLanguages.tags() diverges from the messages*.properties files", fromFiles, fromCode); + } + + @Test + public void norwegianBundleResolvesForNbAndNbNo() { + // A known key whose Norwegian value differs from English, proving the nb file (not the English base) is used. + // Both lookups need the no-fallback control: with the default one, a machine whose locale is nb would resolve + // the English reference through the default-locale fallback to messages_nb and the comparison would invert. + String english = UiLocale.bundle(Locale.ENGLISH).getString("console.exception"); + for (Locale locale : new Locale[] { Locale.forLanguageTag("nb"), Locale.forLanguageTag("nb-NO") }) { + String value = UiLocale.bundle(locale).getString("console.exception"); + assertNotEquals("Expected Norwegian text for " + locale + " but got the English base value", + english, value); + } + } + + @Test + public void isSupportedMatchesLanguageAndRegion() { + assertTrue(SupportedLanguages.isSupported(Locale.forLanguageTag("de"))); + assertTrue(SupportedLanguages.isSupported(Locale.forLanguageTag("zh-CN"))); + assertTrue("nb-NO resolves via language nb", SupportedLanguages.isSupported(Locale.forLanguageTag("nb-NO"))); + assertTrue("pt-BR resolves via language pt", SupportedLanguages.isSupported(Locale.forLanguageTag("pt-BR"))); + } + + /** Maps {@code messages_.properties} file names to BCP-47 tags (underscore -> hyphen), plus base {@code en}. */ + private static TreeSet tagsFromTranslationFiles() throws Exception { + URL url = SupportedLanguagesTest.class.getResource(DEFAULT_BUNDLE); + assertNotNull("Default bundle not on classpath: " + DEFAULT_BUNDLE, url); + Path dir = Paths.get(url.toURI()).getParent(); + TreeSet tags = new TreeSet<>(); + tags.add("en"); + try (DirectoryStream stream = Files.newDirectoryStream(dir, "messages_*.properties")) { + for (Path p : stream) { + String name = p.getFileName().toString(); + String code = name.substring("messages_".length(), name.length() - ".properties".length()); + tags.add(code.replace('_', '-')); + } + } + return tags; + } +} diff --git a/engines/api/src/test/java/net/sf/jsignpdf/utils/UiLocaleTest.java b/engines/api/src/test/java/net/sf/jsignpdf/utils/UiLocaleTest.java new file mode 100644 index 00000000..f15aa9c3 --- /dev/null +++ b/engines/api/src/test/java/net/sf/jsignpdf/utils/UiLocaleTest.java @@ -0,0 +1,121 @@ +package net.sf.jsignpdf.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.Locale; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import net.sf.jsignpdf.Constants; + +/** + * Unit tests for {@link UiLocale}: tag resolution (including underscore normalisation and the legacy {@code no -> nb} + * mapping) and the install, which moves the base default and the DISPLAY category while pinning FORMAT to the OS + * locale, so signed output cannot change. + */ +public class UiLocaleTest { + + private Locale savedDefault; + private Locale savedDisplay; + private Locale savedFormat; + + @Before + public void saveDefaults() { + savedDefault = Locale.getDefault(); + savedDisplay = Locale.getDefault(Locale.Category.DISPLAY); + savedFormat = Locale.getDefault(Locale.Category.FORMAT); + } + + @After + public void restoreDefaults() { + Locale.setDefault(savedDefault); + Locale.setDefault(Locale.Category.DISPLAY, savedDisplay); + Locale.setDefault(Locale.Category.FORMAT, savedFormat); + // init() swaps the process-wide Constants.RES; put it back so test execution order cannot leak a translated + // bundle into a later test in the same surefire fork. + Constants.RES.reload(UiLocale.bundle(savedDisplay)); + } + + @Test + public void resolveKnownTags() { + assertEquals("de", UiLocale.resolve("de").toLanguageTag()); + assertEquals("zh-CN", UiLocale.resolve("zh-CN").toLanguageTag()); + assertEquals("nb", UiLocale.resolve("nb").toLanguageTag()); + assertEquals("nb-NO", UiLocale.resolve("nb-NO").toLanguageTag()); + } + + @Test + public void resolveNormalisesUnderscoreAndTrims() { + assertEquals("zh-CN", UiLocale.resolve("zh_CN").toLanguageTag()); + assertEquals("de", UiLocale.resolve(" de ").toLanguageTag()); + } + + @Test + public void resolveMapsLegacyNorwegianCode() { + assertEquals("nb", UiLocale.resolve("no").toLanguageTag()); + assertEquals("nb", UiLocale.resolve("NO").toLanguageTag()); + assertEquals("nb-NO", UiLocale.resolve("no-NO").toLanguageTag()); + assertEquals("nb-NO", UiLocale.resolve("no_NO").toLanguageTag()); + } + + @Test + public void resolveRejectsEmptyUnknownAndGarbage() { + assertNull(UiLocale.resolve(null)); + assertNull(UiLocale.resolve("")); + assertNull(UiLocale.resolve(" ")); + assertNull("unsupported language", UiLocale.resolve("xx")); + assertNull("garbage", UiLocale.resolve("@@@")); + } + + @Test + public void resolveExpandsRegionlessChinese() { + assertEquals("zh-CN", UiLocale.resolve("zh").toLanguageTag()); + assertEquals("zh-TW", UiLocale.resolve("zh-TW").toLanguageTag()); + } + + @Test + public void canonicalTagMapsToASelectorItem() { + assertEquals("de", UiLocale.canonicalTag("de")); + assertEquals("zh-CN", UiLocale.canonicalTag("zh_CN")); + assertEquals("regional variant collapses to its bundle", "de", UiLocale.canonicalTag("de-AT")); + assertEquals("nb", UiLocale.canonicalTag("nb-NO")); + assertEquals("nb", UiLocale.canonicalTag("no")); + assertEquals("unsupported means System default", "", UiLocale.canonicalTag("xx")); + assertEquals("", UiLocale.canonicalTag("")); + assertEquals("", UiLocale.canonicalTag(null)); + } + + @Test + public void initInstallsBaseAndDisplayButPinsFormatToTheOsLocale() { + Locale.setDefault(Locale.Category.FORMAT, Locale.US); + UiLocale.init(new String[] { "-o", "ui.language=de" }); + assertEquals("de", UiLocale.current().getLanguage()); + assertEquals("DISPLAY category follows the choice", "de", + Locale.getDefault(Locale.Category.DISPLAY).getLanguage()); + // getBundle(baseName) - as JavaFX uses for its own control strings - resolves against the base default, not + // the DISPLAY category, so the base default has to move too or the UI ends up half-translated. + assertEquals("base default follows the choice", "de", Locale.getDefault().getLanguage()); + assertEquals("FORMAT category is left untouched", Locale.US, Locale.getDefault(Locale.Category.FORMAT)); + } + + @Test + public void initKeepsSystemDefaultForUnknownOverride() { + Locale.setDefault(Locale.ITALIAN); + UiLocale.init(new String[] { "-o", "ui.language=xx" }); + assertEquals(UiLocale.systemDefault(), UiLocale.current()); + assertEquals("DISPLAY category left untouched", Locale.ITALIAN, + Locale.getDefault(Locale.Category.DISPLAY)); + assertEquals("base default left untouched", Locale.ITALIAN, Locale.getDefault()); + } + + @Test + public void systemDefaultIgnoresInstalledChoice() { + Locale osLocale = UiLocale.systemDefault(); + UiLocale.init(new String[] { "-o", "ui.language=cs" }); + assertEquals("cs", Locale.getDefault(Locale.Category.DISPLAY).getLanguage()); + assertEquals("systemDefault() reports the OS locale, not the chosen one", osLocale, UiLocale.systemDefault()); + } +} diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java b/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java index b6f5b831..a68a7c1f 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/Signer.java @@ -29,6 +29,7 @@ import net.sf.jsignpdf.utils.GuiUtils; import net.sf.jsignpdf.utils.KeyStoreUtils; import net.sf.jsignpdf.utils.PKCS11Utils; +import net.sf.jsignpdf.utils.UiLocale; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.ParseException; @@ -108,6 +109,9 @@ private static void listEngines() { * @param args */ public static void main(String[] args) { + // Settle the UI locale before Constants.RES / the commons-cli option descriptions capture any strings. + UiLocale.init(args); + SignerOptionsFromCmdLine tmpOpts = null; if (args != null && args.length > 0) { diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/JSignPdfApp.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/JSignPdfApp.java index 75f9d596..c2935ba8 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/JSignPdfApp.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/JSignPdfApp.java @@ -14,6 +14,7 @@ import net.sf.jsignpdf.Constants; import net.sf.jsignpdf.fx.view.MainWindowController; import net.sf.jsignpdf.preview.JpxPluginManager; +import net.sf.jsignpdf.utils.UiLocale; /** * JavaFX Application entry point for JSignPdf. @@ -31,7 +32,7 @@ public void start(Stage primaryStage) throws Exception { // Register any user-installed image codecs (e.g. the optional JPEG 2000 reader) so the preview can use them. JpxPluginManager.registerInstalledPlugins(); - ResourceBundle bundle = ResourceBundle.getBundle(Constants.RESOURCE_BUNDLE_BASE); + ResourceBundle bundle = UiLocale.bundle(); FXMLLoader loader = new FXMLLoader( getClass().getResource("/net/sf/jsignpdf/fx/view/MainWindow.fxml"), bundle); Parent root = loader.load(); diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesController.java index 049c5327..f77ff7d7 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesController.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesController.java @@ -11,6 +11,7 @@ import java.text.MessageFormat; import java.util.Arrays; import java.util.List; +import java.util.Locale; import java.util.Properties; import java.util.ResourceBundle; import java.util.Set; @@ -54,6 +55,8 @@ import net.sf.jsignpdf.utils.FontUtils; import net.sf.jsignpdf.utils.PKCS11Utils; import net.sf.jsignpdf.utils.PropertyStoreFactory; +import net.sf.jsignpdf.utils.SupportedLanguages; +import net.sf.jsignpdf.utils.UiLocale; /** * Controller for the Preferences dialog. Holds the live editing state in a {@link PreferencesViewModel}, mirrors UI changes @@ -78,6 +81,7 @@ public class PreferencesController { @FXML private Tab tabDss; @FXML private Tab tabPkcs11; + @FXML private ChoiceBox cmbUiLanguage; @FXML private ChoiceBox cmbEngine; @FXML private CheckBox chkDebug; @@ -120,6 +124,25 @@ public class PreferencesController { @FXML private void initialize() { + // Language selector: empty tag = "System default", then the bundled translations. + cmbUiLanguage.getItems().add(""); + cmbUiLanguage.getItems().addAll(SupportedLanguages.tags()); + cmbUiLanguage.setConverter(new StringConverter() { + @Override + public String toString(String tag) { + if (tag == null || tag.isEmpty()) { + return RES.get("jfx.gui.preferences.general.language.system", + SupportedLanguages.displayName(UiLocale.systemDefault())); + } + return SupportedLanguages.displayName(Locale.forLanguageTag(tag)); + } + + @Override + public String fromString(String s) { + return null; + } + }); + cmbEngine.getItems().setAll(EngineRegistry.getInstance().listAll()); cmbEngine.setConverter(new StringConverter() { @Override @@ -142,7 +165,7 @@ public SigningEngine fromString(String s) { * dialog modally and persists changes on OK. Returns true if the user pressed OK and the save succeeded. */ public static boolean show(Stage owner) { - ResourceBundle bundle = ResourceBundle.getBundle(Constants.RESOURCE_BUNDLE_BASE); + ResourceBundle bundle = UiLocale.bundle(); FXMLLoader loader = new FXMLLoader( PreferencesController.class.getResource("/net/sf/jsignpdf/fx/view/Preferences.fxml"), bundle); Parent root; @@ -208,6 +231,8 @@ public static boolean show(Stage owner) { void bind(PreferencesViewModel viewModel) { this.vm = viewModel; + cmbUiLanguage.valueProperty().bindBidirectional(vm.uiLanguageProperty()); + // The engine ChoiceBox holds SigningEngine objects; the VM stores the engine id string. Keep the two // in sync manually (both directions, so "Reset section" reselecting the default reflects in the combo). selectEngineById(vm.engineIdProperty().get()); diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModel.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModel.java index 5d2a6f2a..5c0d7cca 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModel.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/preferences/PreferencesViewModel.java @@ -14,6 +14,7 @@ import javafx.beans.property.StringProperty; import net.sf.jsignpdf.utils.AdvancedConfig; import net.sf.jsignpdf.utils.AppConfig; +import net.sf.jsignpdf.utils.UiLocale; /** * Backing model for the Preferences dialog. Loaded from {@link AdvancedConfig} on dialog open and written back on OK. @@ -26,6 +27,7 @@ public class PreferencesViewModel { public static final String LIB_PDFBOX = "pdfbox"; public static final String LIB_OPENPDF = "openpdf"; + private final StringProperty uiLanguage = new SimpleStringProperty(""); private final StringProperty engineId = new SimpleStringProperty(AppConfig.DEFAULT_ENGINE_ID); private final BooleanProperty debug = new SimpleBooleanProperty(false); @@ -65,6 +67,8 @@ public class PreferencesViewModel { /** Loads the VM from the given snapshot of {@link AdvancedConfig} and a pkcs11 file body. */ public void loadFrom(AdvancedConfig cfg, String pkcs11FileBody) { + // Canonicalised so a hand-edited zh_CN / de-AT shows up as the selector item it actually loads. + uiLanguage.set(UiLocale.canonicalTag(cfg.getProperty("ui.language"))); engineId.set(cfg.getNotEmptyProperty("engine", AppConfig.DEFAULT_ENGINE_ID)); debug.set(cfg.getAsBool("debug", false)); fontPath.set(orEmpty(cfg.getProperty("font.path"))); @@ -98,6 +102,7 @@ public void loadFrom(AdvancedConfig cfg, String pkcs11FileBody) { /** Writes the VM back into the given {@link AdvancedConfig} (does not persist; caller should call {@code save()}). */ public void writeTo(AdvancedConfig cfg) { + writeStringOrRemove(cfg, "ui.language", uiLanguage.get()); cfg.setProperty("engine", orFallback(engineId.get(), AppConfig.DEFAULT_ENGINE_ID)); cfg.setProperty("debug", debug.get()); writeStringOrRemove(cfg, "font.path", fontPath.get()); @@ -135,6 +140,7 @@ public void applyDefaults(AdvancedConfig defaults) { } public void applyGeneralDefaults(AdvancedConfig defaults) { + uiLanguage.set(orEmpty(defaults.getBundledDefault("ui.language"))); engineId.set(orFallback(defaults.getBundledDefault("engine"), AppConfig.DEFAULT_ENGINE_ID)); debug.set(parseBool(defaults.getBundledDefault("debug"), false)); } @@ -258,6 +264,7 @@ private static void writeStringOrRemove(AdvancedConfig cfg, String key, String v } } + public StringProperty uiLanguageProperty() { return uiLanguage; } public StringProperty engineIdProperty() { return engineId; } public BooleanProperty debugProperty() { return debug; } public StringProperty fontPathProperty() { return fontPath; } diff --git a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/Preferences.fxml b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/Preferences.fxml index e4124840..2279eba9 100644 --- a/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/Preferences.fxml +++ b/jsignpdf/src/main/resources/net/sf/jsignpdf/fx/view/Preferences.fxml @@ -11,10 +11,18 @@ tabClosingPolicy="UNAVAILABLE" prefWidth="720" prefHeight="520"> - + + + + +