Explorar o código

Merge pull request #2150 from cryptomator/feature/language-switcher

Sebastian Stenzel %!s(int64=3) %!d(string=hai) anos
pai
achega
3136e22414

+ 7 - 0
src/main/java/org/cryptomator/common/settings/Settings.java

@@ -44,6 +44,7 @@ public class Settings {
 	public static final String DEFAULT_LICENSE_KEY = "";
 	public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
 	public static final String DEFAULT_DISPLAY_CONFIGURATION = "";
+	public static final String DEFAULT_LANGUAGE = null;
 
 
 	private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
@@ -66,6 +67,7 @@ public class Settings {
 	private final IntegerProperty windowWidth = new SimpleIntegerProperty();
 	private final IntegerProperty windowHeight = new SimpleIntegerProperty();
 	private final ObjectProperty<String> displayConfiguration = new SimpleObjectProperty<>(DEFAULT_DISPLAY_CONFIGURATION);
+	private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE);
 
 
 	private Consumer<Settings> saveCmd;
@@ -96,6 +98,7 @@ public class Settings {
 		windowWidth.addListener(this::somethingChanged);
 		windowHeight.addListener(this::somethingChanged);
 		displayConfiguration.addListener(this::somethingChanged);
+		language.addListener(this::somethingChanged);
 	}
 
 	void setSaveCmd(Consumer<Settings> saveCmd) {
@@ -191,4 +194,8 @@ public class Settings {
 	public ObjectProperty<String> displayConfigurationProperty() {
 		return displayConfiguration;
 	}
+
+	public StringProperty languageProperty() {
+		return language;
+	}
 }

+ 2 - 0
src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java

@@ -57,6 +57,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
 		out.name("windowWidth").value((value.windowWidthProperty().get()));
 		out.name("windowHeight").value((value.windowHeightProperty().get()));
 		out.name("displayConfiguration").value((value.displayConfigurationProperty().get()));
+		out.name("language").value((value.languageProperty().get()));
 
 		out.endObject();
 	}
@@ -97,6 +98,7 @@ public class SettingsJsonAdapter extends TypeAdapter<Settings> {
 				case "windowWidth" -> settings.windowWidthProperty().set(in.nextInt());
 				case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt());
 				case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString());
+				case "language" -> settings.languageProperty().set(in.nextString());
 
 				default -> {
 					LOG.warn("Unsupported vault setting found in JSON: " + name);

+ 4 - 1
src/main/java/org/cryptomator/launcher/Cryptomator.java

@@ -34,14 +34,16 @@ public class Cryptomator {
 
 	private final LoggerConfiguration logConfig;
 	private final DebugMode debugMode;
+	private final SupportedLanguages supportedLanguages;
 	private final Environment env;
 	private final Lazy<IpcMessageHandler> ipcMessageHandler;
 	private final ShutdownHook shutdownHook;
 
 	@Inject
-	Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) {
+	Cryptomator(LoggerConfiguration logConfig, DebugMode debugMode, SupportedLanguages supportedLanguages, Environment env, Lazy<IpcMessageHandler> ipcMessageHandler, ShutdownHook shutdownHook) {
 		this.logConfig = logConfig;
 		this.debugMode = debugMode;
+		this.supportedLanguages = supportedLanguages;
 		this.env = env;
 		this.ipcMessageHandler = ipcMessageHandler;
 		this.shutdownHook = shutdownHook;
@@ -63,6 +65,7 @@ public class Cryptomator {
 		logConfig.init();
 		LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion().orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
 		debugMode.initialize();
+		supportedLanguages.applyPreferred();
 
 		/*
 		 * Attempts to create an IPC connection to a running Cryptomator instance and sends it the given args.

+ 39 - 0
src/main/java/org/cryptomator/launcher/SupportedLanguages.java

@@ -0,0 +1,39 @@
+package org.cryptomator.launcher;
+
+import org.cryptomator.common.settings.Settings;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import java.util.List;
+import java.util.Locale;
+
+@Singleton
+public class SupportedLanguages {
+
+	private static final Logger LOG = LoggerFactory.getLogger(SupportedLanguages.class);
+	// these are BCP 47 language codes, not ISO. Note the "-" instead of the "_":
+	public static final List<String> LANGUAGAE_TAGS = List.of("en", "ar", "bn", "bs", "ca", "cs", "de", "el", "es", "fil", "fr", "gl", "he", //
+			"hi", "hr", "hu", "id", "it", "ja", "ko", "lv", "mk", "nb", "nl", "nn", "no", "pa", "pl", "pt", "pt-BR", "ro", "ru", "sk", "sr", //
+			"sr-Latn", "sv", "ta", "te", "th", "tr", "uk", "zh", "zh-HK", "zh-TW");
+
+	@Nullable
+	private final String preferredLanguage;
+
+	@Inject
+	public SupportedLanguages(Settings settings) {
+		this.preferredLanguage = settings.languageProperty().get();
+	}
+
+	public void applyPreferred() {
+		if (preferredLanguage == null) {
+			LOG.debug("Using system locale");
+			return;
+		}
+		var preferredLocale = Locale.forLanguageTag(preferredLanguage);
+		LOG.debug("Applying preferred locale {}", preferredLocale.getDisplayName(Locale.ENGLISH));
+		Locale.setDefault(preferredLocale);
+	}
+}

+ 35 - 0
src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java

@@ -1,5 +1,6 @@
 package org.cryptomator.ui.preferences;
 
+import com.google.common.base.Strings;
 import org.cryptomator.common.Environment;
 import org.cryptomator.common.LicenseHolder;
 import org.cryptomator.common.settings.Settings;
@@ -7,6 +8,7 @@ import org.cryptomator.common.settings.UiTheme;
 import org.cryptomator.integrations.autostart.AutoStartProvider;
 import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
 import org.cryptomator.integrations.keychain.KeychainAccessProvider;
+import org.cryptomator.launcher.SupportedLanguages;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
 import org.cryptomator.ui.traymenu.TrayMenuComponent;
@@ -27,6 +29,7 @@ import javafx.scene.control.Toggle;
 import javafx.scene.control.ToggleGroup;
 import javafx.stage.Stage;
 import javafx.util.StringConverter;
+import java.util.Locale;
 import java.util.Optional;
 import java.util.ResourceBundle;
 import java.util.Set;
@@ -53,6 +56,7 @@ public class GeneralPreferencesController implements FxController {
 	public CheckBox showMinimizeButtonCheckbox;
 	public CheckBox showTrayIconCheckbox;
 	public CheckBox startHiddenCheckbox;
+	public ChoiceBox<String> preferredLanguageChoiceBox;
 	public CheckBox debugModeCheckbox;
 	public CheckBox autoStartCheckbox;
 	public ToggleGroup nodeOrientation;
@@ -90,6 +94,11 @@ public class GeneralPreferencesController implements FxController {
 
 		startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
 
+		preferredLanguageChoiceBox.getItems().add(null);
+		preferredLanguageChoiceBox.getItems().addAll(SupportedLanguages.LANGUAGAE_TAGS);
+		preferredLanguageChoiceBox.valueProperty().bindBidirectional(settings.languageProperty());
+		preferredLanguageChoiceBox.setConverter(new LanguageTagConverter(resourceBundle));
+
 		debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());
 
 		autoStartProvider.ifPresent(autoStart -> autoStartCheckbox.setSelected(autoStart.isEnabled()));
@@ -183,6 +192,32 @@ public class GeneralPreferencesController implements FxController {
 
 	}
 
+	private static class LanguageTagConverter extends StringConverter<String> {
+
+		private final ResourceBundle resourceBundle;
+
+		LanguageTagConverter(ResourceBundle resourceBundle) {
+			this.resourceBundle = resourceBundle;
+		}
+
+		@Override
+		public String toString(String tag) {
+			if (tag == null) {
+				return resourceBundle.getString("preferences.general.language.auto");
+			} else {
+				var locale = Locale.forLanguageTag(tag);
+				var lang = locale.getDisplayLanguage(locale);
+				var region = locale.getDisplayCountry(locale);
+				return lang + (Strings.isNullOrEmpty(region) ? "" : " (" + region + ")");
+			}
+		}
+
+		@Override
+		public String fromString(String displayLanguage) {
+			throw new UnsupportedOperationException();
+		}
+	}
+
 	private class KeychainProviderDisplayNameConverter extends StringConverter<KeychainAccessProvider> {
 
 		@Override

+ 5 - 0
src/main/resources/fxml/preferences_general.fxml

@@ -38,6 +38,11 @@
 
 		<CheckBox fx:id="startHiddenCheckbox" text="%preferences.general.startHidden" />
 
+		<HBox spacing="6" alignment="CENTER_LEFT">
+			<Label text="%preferences.general.language"/>
+			<ChoiceBox fx:id="preferredLanguageChoiceBox"/>
+		</HBox>
+
 		<HBox spacing="6" alignment="CENTER_LEFT">
 			<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>
 			<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>

+ 2 - 0
src/main/resources/i18n/strings.properties

@@ -199,6 +199,8 @@ preferences.general.unlockThemes=Unlock dark mode
 preferences.general.showMinimizeButton=Show minimize button
 preferences.general.showTrayIcon=Show tray icon (requires restart)
 preferences.general.startHidden=Hide window when starting Cryptomator
+preferences.general.language=Language (requires restart)
+preferences.general.language.auto=System Default
 preferences.general.debugLogging=Enable debug logging
 preferences.general.debugDirectory=Reveal log files
 preferences.general.autoStart=Launch Cryptomator on system start

+ 2 - 0
src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java

@@ -29,6 +29,7 @@ public class SettingsJsonAdapterTest {
 					],
 					"checkForUpdatesEnabled": true,
 					"port": 8080,
+					"language": "de-DE",
 					"numTrayNotifications": 42,
 					"preferredVolumeImpl": "FUSE"
 				}
@@ -39,6 +40,7 @@ public class SettingsJsonAdapterTest {
 		Assertions.assertTrue(settings.checkForUpdates().get());
 		Assertions.assertEquals(2, settings.getDirectories().size());
 		Assertions.assertEquals(8080, settings.port().get());
+		Assertions.assertEquals("de-DE", settings.languageProperty().get());
 		Assertions.assertEquals(42, settings.numTrayNotifications().get());
 		Assertions.assertEquals(WebDavUrlScheme.DAV, settings.preferredGvfsScheme().get());
 		Assertions.assertEquals(VolumeImpl.FUSE, settings.preferredVolumeImpl().get());

+ 31 - 0
src/test/java/org/cryptomator/launcher/SupportedLanguagesTest.java

@@ -0,0 +1,31 @@
+package org.cryptomator.launcher;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Locale;
+import java.util.ResourceBundle;
+import java.util.stream.Stream;
+
+public class SupportedLanguagesTest {
+
+	@DisplayName("test if resource bundle is localized")
+	@ParameterizedTest(name = "{0}")
+	@MethodSource("languageTags")
+	public void testResourceBundleExists(String tag) {
+		var locale = Locale.forLanguageTag(tag);
+		Assertions.assertNotEquals("und", locale.toLanguageTag(), "Undefined language tag");
+
+		var bundle = Assertions.assertDoesNotThrow(() -> ResourceBundle.getBundle("/i18n/strings", locale));
+
+		Assertions.assertEquals(locale, bundle.getLocale());
+		Assertions.assertFalse(bundle.keySet().isEmpty());
+	}
+
+	public static Stream<String> languageTags() {
+		return SupportedLanguages.LANGUAGAE_TAGS.stream() //
+				.filter(tag -> !"en".equals(tag)); // english uses the default bundle
+	}
+}