summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAdrien Hopkins <adrien.p.hopkins@gmail.com>2025-02-23 20:38:54 -0500
committerAdrien Hopkins <adrien.p.hopkins@gmail.com>2025-02-23 20:38:54 -0500
commit9f85e0c201f64b5de646c4d66323424bcb3a279d (patch)
tree92609c64a7c96fcdd0f20b63bca9c6effe8dd97d
parenta9485b187844cad900bc43c0651406c51a7295c1 (diff)
parent9c358d708ba4988648d7b19ccb842f076ec4c354 (diff)
Merge branch 'i18n' into develop
This merge adds the internationalization features, the final required feature for 7Units version 1.0.0.
-rw-r--r--.classpath2
-rw-r--r--src/main/java/sevenUnitsGUI/Presenter.java808
-rw-r--r--src/main/java/sevenUnitsGUI/TabbedView.java168
-rw-r--r--src/main/java/sevenUnitsGUI/View.java7
-rw-r--r--src/main/java/sevenUnitsGUI/ViewBot.java5
-rw-r--r--src/main/resources/about/en.txt (renamed from src/main/resources/about.txt)2
-rw-r--r--src/main/resources/about/fr.txt24
-rw-r--r--src/main/resources/locales/en.txt31
-rw-r--r--src/main/resources/locales/fr.txt31
-rw-r--r--src/test/java/sevenUnitsGUI/I18nTest.java21
-rw-r--r--src/test/java/sevenUnitsGUI/PresenterTest.java18
11 files changed, 760 insertions, 357 deletions
diff --git a/.classpath b/.classpath
index d4e759d..02a1f84 100644
--- a/.classpath
+++ b/.classpath
@@ -25,6 +25,6 @@
<attribute name="module" value="true"/>
</attributes>
</classpathentry>
- <classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.junit.JUNIT_CONTAINER/5"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>
diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java
index c46ee53..3a039a7 100644
--- a/src/main/java/sevenUnitsGUI/Presenter.java
+++ b/src/main/java/sevenUnitsGUI/Presenter.java
@@ -16,12 +16,12 @@
*/
package sevenUnitsGUI;
-import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -57,7 +57,7 @@ import sevenUnitsGUI.StandardDisplayRules.UncertaintyBased;
/**
* An object that handles interactions between the view and the backend code
- *
+ *
* @author Adrien Hopkins
* @since 2021-12-15
*/
@@ -77,10 +77,23 @@ public final class Presenter {
/** A Predicate that returns true iff the argument is a full base unit */
private static final Predicate<Unit> IS_FULL_BASE = unit -> unit instanceof LinearUnit
&& ((LinearUnit) unit).isBase();
-
+ /**
+ * The default locale, used in two situations:
+ * <ul>
+ * <li>If no text is available in your locale, uses text from this locale.
+ * <li>Users are initialized with this locale.
+ * </ul>
+ */
+ static final String DEFAULT_LOCALE = "en";
+
+ private static final List<String> LOCAL_LOCALES = List.of("en", "fr");
+ private static final Path USER_LOCALES_DIR = userConfigDir()
+ .resolve("SevenUnits").resolve("locales");
+
+
/**
* Adds default units and dimensions to a database.
- *
+ *
* @param database database to add to
* @since 2019-04-14
* @since v0.2.0
@@ -99,20 +112,20 @@ public final class Presenter {
// nonlinear units - must be loaded manually
database.addUnit("tempCelsius", Metric.CELSIUS);
database.addUnit("tempFahrenheit", BritishImperial.FAHRENHEIT);
-
+
// load initial dimensions
database.addDimension("Length", Metric.Dimensions.LENGTH);
database.addDimension("Mass", Metric.Dimensions.MASS);
database.addDimension("Time", Metric.Dimensions.TIME);
database.addDimension("Temperature", Metric.Dimensions.TEMPERATURE);
}
-
+
private static String displayRuleToString(
Function<UncertainDouble, String> numberDisplayRule) {
if (numberDisplayRule instanceof FixedDecimals)
return String.format("FIXED_DECIMALS %d",
((FixedDecimals) numberDisplayRule).decimalPlaces());
- else if (numberDisplayRule instanceof FixedPrecision)
+ if (numberDisplayRule instanceof FixedPrecision)
return String.format("FIXED_PRECISION %d",
((FixedPrecision) numberDisplayRule).significantFigures());
else if (numberDisplayRule instanceof UncertaintyBased)
@@ -120,21 +133,21 @@ public final class Presenter {
else
return numberDisplayRule.toString();
}
-
+
/**
* Determines where to wrap {@code toWrap} with a max line length of
* {@code maxLineLength}. If no good spot is found, returns -1.
*
* @since 2024-08-22
*/
- private static final int findLineSplit(String toWrap, int maxLineLength) {
- for (int i = maxLineLength - 1; i >= 0; i--) {
+ private static int findLineSplit(String toWrap, int maxLineLength) {
+ for (var i = maxLineLength - 1; i >= 0; i--) {
if (Character.isWhitespace(toWrap.charAt(i)))
return i;
}
return -1;
}
-
+
/**
* Gets the text of a resource file as a set of strings (each one is one line
* of the text).
@@ -143,11 +156,11 @@ public final class Presenter {
* @return contents of file
* @since 2021-03-27
*/
- private static final List<String> getLinesFromResource(String filename) {
+ private static List<String> getLinesFromResource(String filename) {
final List<String> lines = new ArrayList<>();
-
- try (InputStream stream = inputStream(filename);
- Scanner scanner = new Scanner(stream)) {
+
+ try (var stream = inputStream(filename);
+ var scanner = new Scanner(stream)) {
while (scanner.hasNextLine()) {
lines.add(scanner.nextLine());
}
@@ -155,10 +168,10 @@ public final class Presenter {
throw new AssertionError(
"Error occurred while loading file " + filename, e);
}
-
+
return lines;
}
-
+
/**
* Gets an input stream for a resource file.
*
@@ -166,98 +179,97 @@ public final class Presenter {
* @return obtained Path
* @since 2021-03-27
*/
- private static final InputStream inputStream(String filepath) {
+ private static InputStream inputStream(String filepath) {
return Presenter.class.getResourceAsStream(filepath);
}
-
+
/**
* Convert a linear unit value to a string, where the number is rounded to
* the nearest integer.
*
* @since 2024-08-16
*/
- private static final String linearUnitValueIntToString(LinearUnitValue uv) {
+ private static String linearUnitValueIntToString(LinearUnitValue uv) {
return Long.toString(Math.round(uv.getValueExact())) + " " + uv.getUnit();
}
-
+
private static Map.Entry<String, String> parseSettingLine(String line) {
- final int equalsIndex = line.indexOf('=');
+ final var equalsIndex = line.indexOf('=');
if (equalsIndex == -1)
throw new IllegalStateException(
"Settings file is malformed at line: " + line);
-
- final String param = line.substring(0, equalsIndex);
- final String value = line.substring(equalsIndex + 1);
-
+
+ final var param = line.substring(0, equalsIndex);
+ final var value = line.substring(equalsIndex + 1);
+
return Map.entry(param, value);
}
-
+
/** Gets a Path from a pathname in the config file. */
private static Path pathFromConfig(String pathname) {
return CONFIG_FILE.getParent().resolve(pathname);
}
-
+
// ====== SETTINGS ======
-
+
private static String searchRuleToString(
Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) {
if (PrefixSearchRule.NO_PREFIXES.equals(searchRule))
return "NO_PREFIXES";
- else if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule))
+ if (PrefixSearchRule.COMMON_PREFIXES.equals(searchRule))
return "COMMON_PREFIXES";
else if (PrefixSearchRule.ALL_METRIC_PREFIXES.equals(searchRule))
return "ALL_METRIC_PREFIXES";
else
return searchRule.toString();
}
-
+
/**
* @return true iff a and b have any elements in common
* @since 2022-04-19
*/
- private static final boolean sharesAnyElements(Set<?> a, Set<?> b) {
+ private static boolean sharesAnyElements(Set<?> a, Set<?> b) {
for (final Object e : a) {
if (b.contains(e))
return true;
}
return false;
}
-
- private static final Path userConfigDir() {
+
+ private static Path userConfigDir() {
if (System.getProperty("os.name").startsWith("Windows")) {
- final String envFolder = System.getenv("LOCALAPPDATA");
+ final var envFolder = System.getenv("LOCALAPPDATA");
if (envFolder == null || "".equals(envFolder))
return Path.of(System.getenv("USERPROFILE"), "AppData", "Local");
else
return Path.of(envFolder);
- } else {
- final String envFolder = System.getenv("XDG_CONFIG_HOME");
- if (envFolder == null || "".equals(envFolder))
- return Path.of(System.getenv("HOME"), ".config");
- else
- return Path.of(envFolder);
}
+ final var envFolder = System.getenv("XDG_CONFIG_HOME");
+ if (envFolder == null || "".equals(envFolder))
+ return Path.of(System.getenv("HOME"), ".config");
+ else
+ return Path.of(envFolder);
}
-
+
/**
* @return {@code line} with any comments removed.
* @since 2021-03-13
*/
- private static final String withoutComments(String line) {
- final int index = line.indexOf('#');
+ private static String withoutComments(String line) {
+ final var index = line.indexOf('#');
return index == -1 ? line : line.substring(0, index);
}
-
+
/**
* Wraps a string, ensuring no line is longer than {@code maxLineLength}.
- *
+ *
* @since 2024-08-22
*/
- private static final String wrapString(String toWrap, int maxLineLength) {
- final StringBuilder wrapped = new StringBuilder(toWrap.length());
- String remaining = toWrap;
+ private static String wrapString(String toWrap, int maxLineLength) {
+ final var wrapped = new StringBuilder(toWrap.length());
+ var remaining = toWrap;
while (remaining.length() > maxLineLength) {
- final int spot = findLineSplit(toWrap, maxLineLength);
+ final var spot = findLineSplit(toWrap, maxLineLength);
if (spot == -1) {
wrapped.append(remaining.substring(0, maxLineLength));
wrapped.append("-\n");
@@ -271,23 +283,23 @@ public final class Presenter {
wrapped.append(remaining);
return wrapped.toString();
}
-
+
/**
* The view that this presenter communicates with
*/
private final View view;
-
+
/**
* The database that this presenter communicates with (effectively the model)
*/
final UnitDatabase database;
-
+
/**
* The rule used for parsing input numbers. Any number-string inputted into
* this program will be parsed using this method. <b>Not implemented yet.</b>
*/
private Function<String, UncertainDouble> numberParsingRule;
-
+
/**
* The rule used for displaying the results of unit conversions. The result
* of unit conversions will be put into this function, and the resulting
@@ -295,35 +307,41 @@ public final class Presenter {
*/
private Function<UncertainDouble, String> numberDisplayRule = StandardDisplayRules
.uncertaintyBased();
-
+
/**
* A predicate that determines whether or not a certain combination of
* prefixes is allowed. If it returns false, a combination of prefixes will
* not be allowed. Prefixes are put in the list from right to left.
*/
private Predicate<List<UnitPrefix>> prefixRepetitionRule = DefaultPrefixRepetitionRule.NO_RESTRICTION;
-
+
/**
* A rule that accepts a prefixless name-unit pair and returns a map mapping
* names to prefixed versions of that unit (including the unit itself) that
* should be searchable.
*/
private Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule = PrefixSearchRule.NO_PREFIXES;
-
+
/**
* The set of units that is considered neither metric nor nonmetric for the
* purposes of the metric-imperial one-way conversion. These units are
* included in both From and To, even if One Way Conversion is enabled.
*/
private final Set<String> metricExceptions;
-
+
+ /** maps locale names (e.g. 'en') to key-text maps */
+ final Map<String, Map<String, String>> locales;
+
+ /** name of locale in locales to use */
+ String userLocale;
+
/**
* If this is true, views that show units as a list will have metric units
* removed from the From unit list and imperial/USC units removed from the To
* unit list.
*/
private boolean oneWayConversionEnabled = false;
-
+
/**
* If this is false, duplicate units and prefixes will be removed from the
* unit view in views that show units as a list to choose from.
@@ -331,39 +349,88 @@ public final class Presenter {
private boolean showDuplicates = false;
/**
+ * The default unit, prefix, dimension and exception data will only be loaded
+ * if this variable is true.
+ */
+ private boolean useDefaultDatafiles = true;
+
+ /** Custom unit datafiles that will be loaded by {@link #reloadData} */
+ private final Set<Path> customUnitFiles = new HashSet<>();
+ /** Custom dimension datafiles that will be loaded by {@link #reloadData} */
+ private final Set<Path> customDimensionFiles = new HashSet<>();
+ /** Custom exception datafiles that will be loaded by {@link #reloadData} */
+ private final Set<Path> customExceptionFiles = new HashSet<>();
+
+ /**
* Creates a Presenter
- *
+ *
* @param view the view that this presenter communicates with
* @since 2021-12-15
*/
public Presenter(View view) {
this.view = view;
this.database = new UnitDatabase();
+ this.metricExceptions = new HashSet<>();
+
+ this.locales = this.loadLocales();
+ this.userLocale = DEFAULT_LOCALE;
+
+ // set default settings temporarily
+ if (Files.exists(CONFIG_FILE)) {
+ this.loadSettings(CONFIG_FILE);
+ }
+
+ this.reloadData();
+
+ // print out unit counts
+ System.out.println(this.loadStatMsg());
+ }
+
+ /**
+ * Clears then reloads all unit, prefix, dimension and exception data.
+ */
+ public void reloadData() {
+ this.database.clear();
+ this.metricExceptions.clear();
addDefaults(this.database);
+ if (this.useDefaultDatafiles) {
+ this.loadDefaultData();
+ }
+
+ this.customUnitFiles.forEach(
+ path -> this.handleLoadErrors(this.database.loadUnitsFile(path)));
+ this.customDimensionFiles.forEach(
+ path -> this.handleLoadErrors(this.database.loadDimensionFile(path)));
+ this.customExceptionFiles.forEach(this::loadExceptionFile);
+ }
+
+ /**
+ * Load units, prefixes and dimensions from the default files.
+ */
+ private void loadDefaultData() {
// load units and prefixes
- try (final InputStream units = inputStream(DEFAULT_UNITS_FILEPATH)) {
+ try (final var units = inputStream(DEFAULT_UNITS_FILEPATH)) {
this.handleLoadErrors(this.database.loadUnitsFromStream(units));
} catch (final IOException e) {
throw new AssertionError("Loading of unitsfile.txt failed.", e);
}
-
+
// load dimensions
- try (final InputStream dimensions = inputStream(
+ try (final var dimensions = inputStream(
DEFAULT_DIMENSIONS_FILEPATH)) {
this.handleLoadErrors(
this.database.loadDimensionsFromStream(dimensions));
} catch (final IOException e) {
throw new AssertionError("Loading of dimensionfile.txt failed.", e);
}
-
+
// load metric exceptions
try {
- this.metricExceptions = new HashSet<>();
- try (InputStream exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH);
- Scanner scanner = new Scanner(exceptions)) {
+ try (var exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH);
+ var scanner = new Scanner(exceptions)) {
while (scanner.hasNextLine()) {
- final String line = Presenter
+ final var line = Presenter
.withoutComments(scanner.nextLine());
if (!line.isBlank()) {
this.metricExceptions.add(line);
@@ -374,16 +441,8 @@ public final class Presenter {
throw new AssertionError("Loading of metric_exceptions.txt failed.",
e);
}
-
- // set default settings temporarily
- if (Files.exists(CONFIG_FILE)) {
- this.loadSettings(CONFIG_FILE);
- }
-
- // print out unit counts
- System.out.println(this.loadStatMsg());
}
-
+
/**
* Applies a search rule to an entry in a name-unit map.
*
@@ -391,23 +450,23 @@ public final class Presenter {
* @return stream of entries, ready for flat-mapping
* @since 2022-07-06
*/
- private final Stream<Map.Entry<String, Unit>> applySearchRule(
+ private Stream<Map.Entry<String, Unit>> applySearchRule(
Map.Entry<String, Unit> e) {
- final Unit u = e.getValue();
+ final var u = e.getValue();
if (u instanceof LinearUnit) {
- final String name = e.getKey();
+ final var name = e.getKey();
final Map.Entry<String, LinearUnit> linearEntry = Map.entry(name,
(LinearUnit) u);
return this.searchRule.apply(linearEntry).entrySet().stream().map(
entry -> Map.entry(entry.getKey(), (Unit) entry.getValue()));
- } else
- return Stream.of(e);
+ }
+ return Stream.of(e);
}
-
+
/**
* Converts from the view's input expression to its output expression.
* Displays an error message if any of the required fields are invalid.
- *
+ *
* @throws UnsupportedOperationException if the view does not support
* expression-based conversion (does
* not implement
@@ -415,49 +474,46 @@ public final class Presenter {
* @since 2021-12-15
*/
public void convertExpressions() {
- if (this.view instanceof ExpressionConversionView) {
- final ExpressionConversionView xcview = (ExpressionConversionView) this.view;
-
- final String fromExpression = xcview.getFromExpression();
- final String toExpression = xcview.getToExpression();
-
- // expressions must not be empty
- if (fromExpression.isEmpty()) {
- this.view.showErrorMessage("Parse Error",
- "Please enter a unit expression in the From: box.");
- return;
- }
- if (toExpression.isEmpty()) {
- this.view.showErrorMessage("Parse Error",
- "Please enter a unit expression in the To: box.");
- return;
- }
-
- final Optional<UnitConversionRecord> uc;
- if (this.database.containsUnitSetName(toExpression)) {
- uc = this.convertExpressionToNamedMultiUnit(fromExpression,
- toExpression);
- } else if (toExpression.contains(";")) {
- final String[] toExpressions = toExpression.split(";");
- uc = this.convertExpressionToMultiUnit(fromExpression,
- toExpressions);
- } else {
- uc = this.convertExpressionToExpression(fromExpression,
- toExpression);
- }
-
- uc.ifPresent(xcview::showExpressionConversionOutput);
- } else
+ if (!(this.view instanceof ExpressionConversionView))
throw new UnsupportedOperationException(
"This function can only be called when the view is an ExpressionConversionView");
+ final var xcview = (ExpressionConversionView) this.view;
+
+ final var fromExpression = xcview.getFromExpression();
+ final var toExpression = xcview.getToExpression();
+
+ // expressions must not be empty
+ if (fromExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the From: box.");
+ return;
+ }
+ if (toExpression.isEmpty()) {
+ this.view.showErrorMessage("Parse Error",
+ "Please enter a unit expression in the To: box.");
+ return;
+ }
+
+ final Optional<UnitConversionRecord> uc;
+ if (this.database.containsUnitSetName(toExpression)) {
+ uc = this.convertExpressionToNamedMultiUnit(fromExpression,
+ toExpression);
+ } else if (toExpression.contains(";")) {
+ final var toExpressions = toExpression.split(";");
+ uc = this.convertExpressionToMultiUnit(fromExpression, toExpressions);
+ } else {
+ uc = this.convertExpressionToExpression(fromExpression, toExpression);
+ }
+
+ uc.ifPresent(xcview::showExpressionConversionOutput);
}
-
+
/**
* Converts a unit expression to another expression.
- *
+ *
* If an error happened, it is shown to the view and Optional.empty() is
* returned.
- *
+ *
* @since 2024-08-15
*/
private Optional<UnitConversionRecord> convertExpressionToExpression(
@@ -479,34 +535,33 @@ public final class Presenter {
"Could not recognize text in To entry: " + e.getMessage());
return Optional.empty();
}
-
+
// convert and show output
- if (from.getUnit().canConvertTo(to)) {
- final UncertainDouble uncertainValue;
-
- // uncertainty is meaningless for non-linear units, so we will have
- // to erase uncertainty information for them
- if (to instanceof LinearUnit) {
- final var toLinear = (LinearUnit) to;
- uncertainValue = from.convertTo(toLinear).getValue();
- } else {
- final double value = from.asUnitValue().convertTo(to).getValue();
- uncertainValue = UncertainDouble.of(value, 0);
- }
-
- final UnitConversionRecord uc = UnitConversionRecord.valueOf(
- fromExpression, toExpression, "",
- this.numberDisplayRule.apply(uncertainValue));
- return Optional.of(uc);
- } else {
+ if (!from.getUnit().canConvertTo(to)) {
this.view.showErrorMessage("Conversion Error",
"Cannot convert between \"" + fromExpression + "\" and \""
+ toExpression + "\".");
return Optional.empty();
}
-
+ final UncertainDouble uncertainValue;
+
+ // uncertainty is meaningless for non-linear units, so we will have
+ // to erase uncertainty information for them
+ if (to instanceof LinearUnit) {
+ final var toLinear = (LinearUnit) to;
+ uncertainValue = from.convertTo(toLinear).getValue();
+ } else {
+ final var value = from.asUnitValue().convertTo(to).getValue();
+ uncertainValue = UncertainDouble.of(value, 0);
+ }
+
+ final var uc = UnitConversionRecord.valueOf(
+ fromExpression, toExpression, "",
+ this.numberDisplayRule.apply(uncertainValue));
+ return Optional.of(uc);
+
}
-
+
/**
* Convert an expression to a MultiUnit. If an error happened, it is shown to
* the view and Optional.empty() is returned.
@@ -523,27 +578,26 @@ public final class Presenter {
"Could not recognize text in From entry: " + e.getMessage());
return Optional.empty();
}
-
+
final List<LinearUnit> toUnits = new ArrayList<>(toExpressions.length);
- for (int i = 0; i < toExpressions.length; i++) {
+ for (final String toExpression : toExpressions) {
try {
- final Unit toI = this.database
- .getUnitFromExpression(toExpressions[i].trim());
- if (toI instanceof LinearUnit) {
- toUnits.add((LinearUnit) toI);
- } else {
+ final var toI = this.database
+ .getUnitFromExpression(toExpression.trim());
+ if (!(toI instanceof LinearUnit)) {
this.view.showErrorMessage("Unit Type Error",
"Units separated by ';' must be linear; " + toI
+ " is not.");
return Optional.empty();
}
+ toUnits.add((LinearUnit) toI);
} catch (final IllegalArgumentException | NoSuchElementException e) {
this.view.showErrorMessage("Parse Error",
"Could not recognize text in To entry: " + e.getMessage());
return Optional.empty();
}
}
-
+
final List<LinearUnitValue> toValues;
try {
toValues = from.convertToMultiple(toUnits);
@@ -552,12 +606,12 @@ public final class Presenter {
"Invalid units separated by ';': " + e.getMessage());
return Optional.empty();
}
-
- final String toExpression = this.linearUnitValueSumToString(toValues);
+
+ final var toExpression = this.linearUnitValueSumToString(toValues);
return Optional.of(
UnitConversionRecord.valueOf(fromExpression, toExpression, "", ""));
}
-
+
/**
* Convert an expression to a MultiUnit with a name from the database. If an
* error happened, it is shown to the view and Optional.empty() is returned.
@@ -574,8 +628,8 @@ public final class Presenter {
"Could not recognize text in From entry: " + e.getMessage());
return Optional.empty();
}
-
- final List<LinearUnit> toUnits = this.database.getUnitSet(toName);
+
+ final var toUnits = this.database.getUnitSet(toName);
final List<LinearUnitValue> toValues;
try {
toValues = from.convertToMultiple(toUnits);
@@ -584,16 +638,16 @@ public final class Presenter {
"Invalid units separated by ';': " + e.getMessage());
return Optional.empty();
}
-
- final String toExpression = this.linearUnitValueSumToString(toValues);
+
+ final var toExpression = this.linearUnitValueSumToString(toValues);
return Optional.of(
UnitConversionRecord.valueOf(fromExpression, toExpression, "", ""));
}
-
+
/**
* Converts from the view's input unit to its output unit. Displays an error
* message if any of the required fields are invalid.
- *
+ *
* @throws UnsupportedOperationException if the view does not support
* unit-based conversion (does not
* implement
@@ -601,64 +655,61 @@ public final class Presenter {
* @since 2021-12-15
*/
public void convertUnits() {
- if (this.view instanceof UnitConversionView) {
- final UnitConversionView ucview = (UnitConversionView) this.view;
-
- final Optional<String> fromUnitOptional = ucview.getFromSelection();
- final Optional<String> toUnitOptional = ucview.getToSelection();
- final String inputValueString = ucview.getInputValue();
-
- // extract values from optionals
- final String fromUnitString, toUnitString;
- if (fromUnitOptional.isPresent()) {
- fromUnitString = fromUnitOptional.orElseThrow();
- } else {
- this.view.showErrorMessage("Unit Selection Error",
- "Please specify a From unit");
- return;
- }
- if (toUnitOptional.isPresent()) {
- toUnitString = toUnitOptional.orElseThrow();
- } else {
- this.view.showErrorMessage("Unit Selection Error",
- "Please specify a To unit");
- return;
- }
-
- // convert strings to data, checking if anything is invalid
- final Unit fromUnit;
- final UncertainDouble uncertainValue;
-
- if (this.database.containsUnitName(fromUnitString)) {
- fromUnit = this.database.getUnit(fromUnitString);
- } else
- throw this.viewError("Nonexistent From unit: %s", fromUnitString);
- try {
- uncertainValue = UncertainDouble
- .fromRoundedString(inputValueString);
- } catch (final NumberFormatException e) {
- this.view.showErrorMessage("Value Error",
- "Invalid value " + inputValueString);
- return;
- }
- if (this.database.containsUnitName(toUnitString)) {
- final Unit toUnit = this.database.getUnit(toUnitString);
- ucview.showUnitConversionOutput(
- this.convertUnitToUnit(fromUnitString, toUnitString,
- inputValueString, fromUnit, toUnit, uncertainValue));
- } else if (this.database.containsUnitSetName(toUnitString)) {
- final List<LinearUnit> toMulti = this.database
- .getUnitSet(toUnitString);
- ucview.showUnitConversionOutput(
- this.convertUnitToMulti(fromUnitString, inputValueString,
- fromUnit, toMulti, uncertainValue));
- } else
- throw this.viewError("Nonexistent To unit: %s", toUnitString);
- } else
+ if (!(this.view instanceof UnitConversionView))
throw new UnsupportedOperationException(
"This function can only be called when the view is a UnitConversionView.");
+ final var ucview = (UnitConversionView) this.view;
+
+ final var fromUnitOptional = ucview.getFromSelection();
+ final var toUnitOptional = ucview.getToSelection();
+ final var inputValueString = ucview.getInputValue();
+
+ // extract values from optionals
+ final String fromUnitString, toUnitString;
+ if (fromUnitOptional.isPresent()) {
+ fromUnitString = fromUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a From unit");
+ return;
+ }
+ if (toUnitOptional.isPresent()) {
+ toUnitString = toUnitOptional.orElseThrow();
+ } else {
+ this.view.showErrorMessage("Unit Selection Error",
+ "Please specify a To unit");
+ return;
+ }
+
+ // convert strings to data, checking if anything is invalid
+ final Unit fromUnit;
+ final UncertainDouble uncertainValue;
+
+ if (this.database.containsUnitName(fromUnitString)) {
+ fromUnit = this.database.getUnit(fromUnitString);
+ } else
+ throw this.viewError("Nonexistent From unit: %s", fromUnitString);
+ try {
+ uncertainValue = UncertainDouble.fromRoundedString(inputValueString);
+ } catch (final NumberFormatException e) {
+ this.view.showErrorMessage("Value Error",
+ "Invalid value " + inputValueString);
+ return;
+ }
+ if (this.database.containsUnitName(toUnitString)) {
+ final var toUnit = this.database.getUnit(toUnitString);
+ ucview.showUnitConversionOutput(
+ this.convertUnitToUnit(fromUnitString, toUnitString,
+ inputValueString, fromUnit, toUnit, uncertainValue));
+ } else if (this.database.containsUnitSetName(toUnitString)) {
+ final var toMulti = this.database
+ .getUnitSet(toUnitString);
+ ucview.showUnitConversionOutput(this.convertUnitToMulti(fromUnitString,
+ inputValueString, fromUnit, toMulti, uncertainValue));
+ } else
+ throw this.viewError("Nonexistent To unit: %s", toUnitString);
}
-
+
private UnitConversionRecord convertUnitToMulti(String fromUnitString,
String inputValueString, Unit fromUnit, List<LinearUnit> toMulti,
UncertainDouble uncertainValue) {
@@ -667,7 +718,7 @@ public final class Presenter {
throw this.viewError("Could not convert between %s and %s",
fromUnit, toUnit);
}
-
+
final LinearUnitValue initValue;
if (fromUnit instanceof LinearUnit) {
final var fromLinear = (LinearUnit) fromUnit;
@@ -676,46 +727,46 @@ public final class Presenter {
initValue = UnitValue.of(fromUnit, uncertainValue.value())
.convertToBase(NameSymbol.EMPTY);
}
-
- final List<LinearUnitValue> converted = initValue
+
+ final var converted = initValue
.convertToMultiple(toMulti);
- final String toExpression = this.linearUnitValueSumToString(converted);
+ final var toExpression = this.linearUnitValueSumToString(converted);
return UnitConversionRecord.valueOf(fromUnitString, toExpression,
inputValueString, "");
-
+
}
-
+
private UnitConversionRecord convertUnitToUnit(String fromUnitString,
String toUnitString, String inputValueString, Unit fromUnit,
Unit toUnit, UncertainDouble uncertainValue) {
if (!fromUnit.canConvertTo(toUnit))
throw this.viewError("Could not convert between %s and %s", fromUnit,
toUnit);
-
+
// convert - we will need to erase uncertainty for non-linear units, so
// we need to treat linear and non-linear units differently
final String outputValueString;
if (fromUnit instanceof LinearUnit && toUnit instanceof LinearUnit) {
- final LinearUnit fromLinear = (LinearUnit) fromUnit;
- final LinearUnit toLinear = (LinearUnit) toUnit;
- final LinearUnitValue initialValue = LinearUnitValue.of(fromLinear,
+ final var fromLinear = (LinearUnit) fromUnit;
+ final var toLinear = (LinearUnit) toUnit;
+ final var initialValue = LinearUnitValue.of(fromLinear,
uncertainValue);
- final LinearUnitValue converted = initialValue.convertTo(toLinear);
-
+ final var converted = initialValue.convertTo(toLinear);
+
outputValueString = this.numberDisplayRule.apply(converted.getValue());
} else {
- final UnitValue initialValue = UnitValue.of(fromUnit,
+ final var initialValue = UnitValue.of(fromUnit,
uncertainValue.value());
- final UnitValue converted = initialValue.convertTo(toUnit);
-
+ final var converted = initialValue.convertTo(toUnit);
+
outputValueString = this.numberDisplayRule
.apply(UncertainDouble.of(converted.getValue(), 0));
}
-
+
return UnitConversionRecord.valueOf(fromUnitString, toUnitString,
inputValueString, outputValueString);
}
-
+
/**
* @return true iff duplicate units are shown in unit lists
* @since 2022-03-30
@@ -723,18 +774,46 @@ public final class Presenter {
public boolean duplicatesShown() {
return this.showDuplicates;
}
-
+
/**
* @return text in About file
* @since 2022-02-19
*/
- public final String getAboutText() {
- return Presenter.getLinesFromResource("/about.txt").stream()
+ public String getAboutText() {
+ final Path customFilepath = Presenter.pathFromConfig(
+ "about/" + this.userLocale + ".txt");
+ if (Files.exists(customFilepath)) {
+ try {
+ return formatAboutText(Files.lines(customFilepath));
+ } catch (IOException e) {
+ final String filename = String.format("/about/%s.txt", this.userLocale);
+ return formatAboutText(Presenter.getLinesFromResource(filename).stream());
+ }
+ } else if (LOCAL_LOCALES.contains(this.userLocale)) {
+ final String filename = String.format("/about/%s.txt", this.userLocale);
+ return formatAboutText(Presenter.getLinesFromResource(filename).stream());
+ } else {
+ final String filename = String.format("/about/%s.txt", DEFAULT_LOCALE);
+ return formatAboutText(Presenter.getLinesFromResource(filename).stream());
+ }
+
+ }
+
+ private String formatAboutText(Stream<String> rawLines) {
+ return rawLines
.map(Presenter::withoutComments).collect(Collectors.joining("\n"))
.replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString())
.replaceAll("\\[LOADSTATS\\]", wrapString(this.loadStatMsg(), 72));
}
-
+
+ /**
+ * @return set of all locales available to select
+ * @since 2025-02-21
+ */
+ public Set<String> getAvailableLocales() {
+ return this.locales.keySet();
+ }
+
/**
* Gets a name for this dimension using the database
*
@@ -742,14 +821,28 @@ public final class Presenter {
* @return name of dimension
* @since 2022-04-16
*/
- final String getDimensionName(ObjectProduct<BaseDimension> dimension) {
+ String getDimensionName(ObjectProduct<BaseDimension> dimension) {
// find this dimension in the database and get its name
// if it isn't there, use the dimension's toString instead
return this.database.dimensionMap().values().stream()
.filter(d -> d.equals(dimension)).findAny().map(Nameable::getName)
.orElse(dimension.toString(Nameable::getName));
}
-
+
+ /**
+ * Gets the correct text for a provided ID. If this text is available in the
+ * user's locale, that text is provided. Otherwise, text is taken from the
+ * system default locale {@link #DEFAULT_LOCALE}.
+ *
+ * @param textID ID of text to get (used in locale files)
+ * @return text to be displayed
+ */
+ public String getLocalizedText(String textID) {
+ final var userLocale = this.locales.get(this.userLocale);
+ final var defaultLocale = this.locales.get(DEFAULT_LOCALE);
+ return userLocale.getOrDefault(textID, defaultLocale.get(textID));
+ }
+
/**
* @return the rule that is used by this presenter to convert numbers into
* strings
@@ -758,7 +851,7 @@ public final class Presenter {
public Function<UncertainDouble, String> getNumberDisplayRule() {
return this.numberDisplayRule;
}
-
+
/**
* @return the rule that is used by this presenter to convert strings into
* numbers
@@ -768,7 +861,7 @@ public final class Presenter {
private Function<String, UncertainDouble> getNumberParsingRule() {
return this.numberParsingRule;
}
-
+
/**
* @return the rule that determines whether a set of prefixes is valid
* @since 2022-04-19
@@ -776,7 +869,7 @@ public final class Presenter {
public Predicate<List<UnitPrefix>> getPrefixRepetitionRule() {
return this.prefixRepetitionRule;
}
-
+
/**
* @return the rule that determines which units are prefixed
* @since 2022-07-08
@@ -784,7 +877,7 @@ public final class Presenter {
public Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> getSearchRule() {
return this.searchRule;
}
-
+
/**
* @return a search rule that shows all single prefixes
* @since 2022-07-08
@@ -793,7 +886,15 @@ public final class Presenter {
return PrefixSearchRule.getCoherentOnlyRule(
new HashSet<>(this.database.prefixMap(true).values()));
}
-
+
+ /**
+ * @return user's selected locale
+ * @since 2025-02-21
+ */
+ public String getUserLocale() {
+ return userLocale;
+ }
+
/**
* @return the view associated with this presenter
* @since 2022-04-19
@@ -801,7 +902,7 @@ public final class Presenter {
public View getView() {
return this.view;
}
-
+
/**
* Accepts a list of errors. If that list is non-empty, prints an error
* message and alerts the user.
@@ -810,7 +911,7 @@ public final class Presenter {
*/
private void handleLoadErrors(List<LoadingException> errors) {
if (!errors.isEmpty()) {
- final String errorMessage = String.format(
+ final var errorMessage = String.format(
"%d error(s) happened while loading file:\n%s\n", errors.size(),
errors.stream().map(Throwable::getMessage)
.collect(Collectors.joining("\n")));
@@ -819,13 +920,13 @@ public final class Presenter {
errorMessage);
}
}
-
+
/**
* @return whether or not the provided unit is semi-metric (i.e. an
* exception)
* @since 2022-04-16
*/
- final boolean isSemiMetric(Unit u) {
+ boolean isSemiMetric(Unit u) {
// determine if u is an exception
final var primaryName = u.getPrimaryName();
final var symbol = u.getSymbol();
@@ -835,7 +936,7 @@ public final class Presenter {
&& this.metricExceptions.contains(symbol.orElseThrow())
|| sharesAnyElements(this.metricExceptions, u.getOtherNames());
}
-
+
/**
* Convert a list of LinearUnitValues that you would get from a unit-set
* conversion to a string. All but the last have their numbers rendered as
@@ -844,18 +945,17 @@ public final class Presenter {
*
* @since 2024-08-16
*/
- private final String linearUnitValueSumToString(
- List<LinearUnitValue> values) {
- final String integerPart = values.subList(0, values.size() - 1).stream()
+ private String linearUnitValueSumToString(List<LinearUnitValue> values) {
+ final var integerPart = values.subList(0, values.size() - 1).stream()
.map(Presenter::linearUnitValueIntToString)
.collect(Collectors.joining(" + "));
- final LinearUnitValue last = values.get(values.size() - 1);
+ final var last = values.get(values.size() - 1);
return integerPart + " + " + this.numberDisplayRule.apply(last.getValue())
+ " " + last.getUnit();
}
-
+
private void loadExceptionFile(Path exceptionFile) {
- try (Stream<String> lines = Files.lines(exceptionFile)) {
+ try (var lines = Files.lines(exceptionFile)) {
lines.map(Presenter::withoutComments)
.forEach(this.metricExceptions::add);
} catch (final IOException e) {
@@ -864,7 +964,54 @@ public final class Presenter {
+ exceptionFile + "\": " + e.getLocalizedMessage());
}
}
+
+ /**
+ * Loads all available locales, including custom ones, into a map.
+ * @return map containing locales
+ */
+ private Map<String, Map<String, String>> loadLocales() {
+ final Map<String, Map<String, String>> locales = new HashMap<>();
+ for (final String localeName : LOCAL_LOCALES) {
+ final Map<String, String> locale = new HashMap<>();
+ String filename = "/locales/" + localeName + ".txt";
+ getLinesFromResource(filename).forEach(line -> addLocaleLine(locale, line));
+ locales.put(localeName, locale);
+ }
+
+ if (Files.exists(USER_LOCALES_DIR)) {
+ try {
+ Files.list(USER_LOCALES_DIR).forEach(
+ localeFile -> {
+ try {
+ addLocaleFile(locales, localeFile);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ return locales;
+ }
+ private void addLocaleFile(Map<String, Map<String, String>> locales, Path file) throws IOException {
+ final Map<String, String> locale = new HashMap<>();
+ String fileName = file.getName(file.getNameCount()-1).toString();
+ String localeName = fileName.substring(0, fileName.length()-4);
+ Files.lines(file).forEach(line -> addLocaleLine(locale, line));
+ locales.put(localeName, locale);
+ }
+
+ private void addLocaleLine(Map<String, String> locale, String line) {
+ String[] parts = line.split("=", 2);
+ if (parts.length < 2) {
+ return;
+ }
+
+ locale.put(parts[0], parts[1]);
+ }
+
/**
* Loads settings from the user's settings file and applies them to the
* presenter.
@@ -873,23 +1020,25 @@ public final class Presenter {
* @since 2021-12-15
*/
void loadSettings(Path settingsFile) {
+ this.customDimensionFiles.clear();
+ this.customExceptionFiles.clear();
+ this.customUnitFiles.clear();
+
for (final Map.Entry<String, String> setting : this
.settingsFromFile(settingsFile)) {
- final String value = setting.getValue();
-
+ final var value = setting.getValue();
+
switch (setting.getKey()) {
// set manually to avoid the unnecessary saving of the non-manual
// methods
case "custom_dimension_file":
- this.handleLoadErrors(
- this.database.loadDimensionFile(pathFromConfig(value)));
+ this.customDimensionFiles.add(pathFromConfig(value));
break;
case "custom_exception_file":
- this.loadExceptionFile(pathFromConfig(value));
+ this.customExceptionFiles.add(pathFromConfig(value));
break;
case "custom_unit_file":
- this.handleLoadErrors(
- this.database.loadUnitsFile(pathFromConfig(value)));
+ this.customUnitFiles.add(pathFromConfig(value));
break;
case "number_display_rule":
this.setDisplayRuleFromString(value);
@@ -900,73 +1049,78 @@ public final class Presenter {
this.database.setPrefixRepetitionRule(this.prefixRepetitionRule);
break;
case "one_way":
- this.oneWayConversionEnabled = Boolean.valueOf(value);
+ this.oneWayConversionEnabled = Boolean.parseBoolean(value);
break;
case "include_duplicates":
- this.showDuplicates = Boolean.valueOf(value);
+ this.showDuplicates = Boolean.parseBoolean(value);
break;
case "search_prefix_rule":
this.setSearchRuleFromString(value);
break;
+ case "locale":
+ this.userLocale = value;
+ break;
default:
System.err.printf("Warning: unrecognized setting \"%s\".%n",
setting.getKey());
break;
}
}
-
+
if (this.view.getPresenter() != null) {
this.updateView();
}
}
-
+
/**
* @return a message showing how much stuff has been loaded
* @since 2024-08-22
*/
private String loadStatMsg() {
- return String.format(
- "Successfully loaded %d unique units with %d names (%d base units), %d unique prefixes with %d names, %d unit sets, and %d named dimensions.",
- this.database.unitMapPrefixless(false).size(),
- this.database.unitMapPrefixless(true).size(),
- this.database.unitMapPrefixless(false).values().stream()
- .filter(IS_FULL_BASE).count(),
- this.database.prefixMap(false).size(),
- this.database.prefixMap(true).size(),
- this.database.unitSetMap().size(),
- this.database.dimensionMap().size());
+ return this.getLocalizedText("load_stat_msg")
+ .replace("[u]", Integer.toString(
+ this.database.unitMapPrefixless(false).size()))
+ .replace("[un]", Integer.toString(
+ this.database.unitMapPrefixless(true).size()))
+ .replace("[b]", Long.toString(this.database.unitMapPrefixless(false)
+ .values().stream().filter(IS_FULL_BASE).count()))
+ .replace("[p]", Integer.toString(this.database.prefixMap(false).size()))
+ .replace("[pn]", Integer.toString(this.database.prefixMap(true).size()))
+ .replace("[s]", Integer.toString(this.database.unitSetMap().size()))
+ .replace("[d]", Integer.toString(this.database.dimensionMap().size()));
}
-
+
/**
* @return true iff the One-Way Conversion feature is available (views that
* show units as a list will have metric units removed from the From
* unit list and imperial/USC units removed from the To unit list)
- *
+ *
* @since 2022-03-30
*/
public boolean oneWayConversionEnabled() {
return this.oneWayConversionEnabled;
}
-
+
/**
* Completes creation of the presenter. This part of the initialization
* depends on the view's functions, so it cannot be run if the components
* they depend on are not created yet.
- *
+ *
* @since 2022-02-26
*/
public void postViewInitialize() {
// unit conversion specific stuff
if (this.view instanceof UnitConversionView) {
- final UnitConversionView ucview = (UnitConversionView) this.view;
+ final var ucview = (UnitConversionView) this.view;
ucview.setDimensionNames(this.database.dimensionMap().keySet());
}
-
+
this.updateView();
+ this.view.updateText();
}
-
+
void prefixSelected() {
- final Optional<String> selectedPrefixName = this.view
+ final var selectedPrefixName = this.view
.getViewedPrefixName();
final Optional<UnitPrefix> selectedPrefix = selectedPrefixName
.map(name -> this.database.containsPrefixName(name)
@@ -976,16 +1130,16 @@ public final class Presenter {
.ifPresent(prefix -> this.view.showPrefix(prefix.getNameSymbol(),
String.valueOf(prefix.getMultiplier())));
}
-
+
/**
* Saves the presenter's current settings to the config file, creating it if
* it doesn't exist.
- *
+ *
* @return false iff the presenter could not write to the file
* @since 2022-04-19
*/
public boolean saveSettings() {
- final Path configDir = CONFIG_FILE.getParent();
+ final var configDir = CONFIG_FILE.getParent();
if (!Files.exists(configDir)) {
try {
Files.createDirectories(configDir);
@@ -993,19 +1147,19 @@ public final class Presenter {
return false;
}
}
-
+
return this.writeSettings(CONFIG_FILE);
}
-
+
private void setDisplayRuleFromString(String ruleString) {
- final String[] tokens = ruleString.split(" ");
+ final var tokens = ruleString.split(" ");
switch (tokens[0]) {
case "FIXED_DECIMALS":
- final int decimals = Integer.parseInt(tokens[1]);
+ final var decimals = Integer.parseInt(tokens[1]);
this.numberDisplayRule = StandardDisplayRules.fixedDecimals(decimals);
break;
case "FIXED_PRECISION":
- final int sigDigs = Integer.parseInt(tokens[1]);
+ final var sigDigs = Integer.parseInt(tokens[1]);
this.numberDisplayRule = StandardDisplayRules.fixedPrecision(sigDigs);
break;
case "UNCERTAINTY_BASED":
@@ -1017,7 +1171,7 @@ public final class Presenter {
break;
}
}
-
+
/**
* @param numberDisplayRule the new rule that will be used by this presenter
* to convert numbers into strings
@@ -1027,7 +1181,7 @@ public final class Presenter {
Function<UncertainDouble, String> numberDisplayRule) {
this.numberDisplayRule = numberDisplayRule;
}
-
+
/**
* @param numberParsingRule the new rule that will be used by this presenter
* to convert strings into numbers
@@ -1038,7 +1192,7 @@ public final class Presenter {
Function<String, UncertainDouble> numberParsingRule) {
this.numberParsingRule = numberParsingRule;
}
-
+
/**
* @param oneWayConversionEnabled whether not one-way conversion should be
* enabled
@@ -1049,7 +1203,7 @@ public final class Presenter {
this.oneWayConversionEnabled = oneWayConversionEnabled;
this.updateView();
}
-
+
/**
* @param prefixRepetitionRule the rule that determines whether a set of
* prefixes is valid
@@ -1060,7 +1214,7 @@ public final class Presenter {
this.prefixRepetitionRule = prefixRepetitionRule;
this.database.setPrefixRepetitionRule(prefixRepetitionRule);
}
-
+
/**
* @param searchRule A rule that accepts a prefixless name-unit pair and
* returns a map mapping names to prefixed versions of that
@@ -1072,7 +1226,7 @@ public final class Presenter {
Function<Map.Entry<String, LinearUnit>, Map<String, LinearUnit>> searchRule) {
this.searchRule = searchRule;
}
-
+
private void setSearchRuleFromString(String ruleString) {
switch (ruleString) {
case "NO_PREFIXES":
@@ -1090,7 +1244,7 @@ public final class Presenter {
ruleString);
}
}
-
+
/**
* @param showDuplicateUnits whether or not duplicate units should be shown
* @since 2022-03-30
@@ -1099,9 +1253,9 @@ public final class Presenter {
this.showDuplicates = showDuplicateUnits;
this.updateView();
}
-
+
private List<Map.Entry<String, String>> settingsFromFile(Path settingsFile) {
- try (Stream<String> lines = Files.lines(settingsFile)) {
+ try (var lines = Files.lines(settingsFile)) {
return lines.map(Presenter::withoutComments)
.filter(line -> !line.isBlank()).map(Presenter::parseSettingLine)
.toList();
@@ -1113,6 +1267,26 @@ public final class Presenter {
}
/**
+ * Sets whether or not the default datafiles will be loaded.
+ * This method automatically updates the view's units.
+ */
+ public void setUseDefaultDatafiles(boolean useDefaultDatafiles) {
+ this.useDefaultDatafiles = useDefaultDatafiles;
+ this.reloadData();
+ this.updateView();
+ }
+
+ /**
+ * Sets the user's locale, updating the view.
+ *
+ * @param userLocale locale to use
+ */
+ public void setUserLocale(String userLocale) {
+ this.userLocale = userLocale;
+ this.view.updateText();
+ }
+
+ /**
* Shows a unit in the unit viewer
*
* @param u unit to show
@@ -1120,53 +1294,53 @@ public final class Presenter {
*/
private void showUnit(Unit u) {
final var nameSymbol = u.getNameSymbol();
- final boolean isBase = u instanceof BaseUnit
+ final var isBase = u instanceof BaseUnit
|| u instanceof LinearUnit && ((LinearUnit) u).isBase();
final var definition = isBase ? "(Base unit)" : u.toDefinitionString();
final var dimensionString = this.getDimensionName(u.getDimension());
final var unitType = UnitType.getType(u, this::isSemiMetric);
this.view.showUnit(nameSymbol, definition, dimensionString, unitType);
}
-
+
/**
* Runs whenever a unit name is selected in the unit viewer. Gets the
* description of a unit and displays it.
- *
+ *
* @since 2022-04-10
*/
void unitNameSelected() {
// get selected unit, if it's there and valid
- final Optional<String> selectedUnitName = this.view.getViewedUnitName();
+ final var selectedUnitName = this.view.getViewedUnitName();
final Optional<Unit> selectedUnit = selectedUnitName
.map(unitName -> this.database.containsUnitName(unitName)
? this.database.getUnit(unitName)
: null);
selectedUnit.ifPresent(this::showUnit);
}
-
+
/**
* Updates the view's From and To units, if it has some
- *
+ *
* @since 2021-12-15
*/
public void updateView() {
if (this.view instanceof UnitConversionView) {
- final UnitConversionView ucview = (UnitConversionView) this.view;
+ final var ucview = (UnitConversionView) this.view;
final var selectedDimensionName = ucview.getSelectedDimensionName();
-
+
// load units & prefixes into viewers
this.view.setViewableUnitNames(
this.database.unitMapPrefixless(this.showDuplicates).keySet());
this.view.setViewablePrefixNames(
this.database.prefixMap(this.showDuplicates).keySet());
-
+
// get From and To units
var fromUnits = this.database.unitMapPrefixless(this.showDuplicates)
.entrySet().stream();
var toUnits = this.database.unitMapPrefixless(this.showDuplicates)
.entrySet().stream();
var unitSets = this.database.unitSetMap().entrySet().stream();
-
+
// filter by dimension, if one is selected
if (selectedDimensionName.isPresent()) {
final var viewDimension = this.database
@@ -1178,7 +1352,7 @@ public final class Presenter {
unitSets = unitSets.filter(us -> viewDimension
.equals(us.getValue().get(0).getDimension()));
}
-
+
// filter by unit type, if desired
if (this.oneWayConversionEnabled) {
fromUnits = fromUnits.filter(u -> UnitType.getType(u.getValue(),
@@ -1189,7 +1363,7 @@ public final class Presenter {
unitSets = unitSets
.filter(us -> this.metricExceptions.contains(us.getKey()));
}
-
+
// set unit names
ucview.setFromUnitNames(fromUnits.flatMap(this::applySearchRule)
.map(Map.Entry::getKey).collect(Collectors.toSet()));
@@ -1203,6 +1377,13 @@ public final class Presenter {
}
/**
+ * @return true iff the default datafiles are being used
+ */
+ public boolean usingDefaultDatafiles() {
+ return this.useDefaultDatafiles;
+ }
+
+ /**
* @param message message to add
* @param args string formatting arguments for message
* @return AssertionError stating that an error has happened in the view's
@@ -1213,15 +1394,15 @@ public final class Presenter {
return new AssertionError("View Programming Error (from " + this.view
+ "): " + String.format(message, args));
}
-
+
/**
* Saves the presenter's settings to the user settings file.
- *
+ *
* @param settingsFile file settings should be saved to
* @since 2021-12-15
*/
boolean writeSettings(Path settingsFile) {
- try (BufferedWriter writer = Files.newBufferedWriter(settingsFile)) {
+ try (var writer = Files.newBufferedWriter(settingsFile)) {
writer.write(String.format("number_display_rule=%s\n",
displayRuleToString(this.numberDisplayRule)));
writer.write(
@@ -1232,6 +1413,7 @@ public final class Presenter {
String.format("include_duplicates=%s\n", this.showDuplicates));
writer.write(String.format("search_prefix_rule=%s\n",
searchRuleToString(this.searchRule)));
+ writer.write(String.format("locale=%s\n", this.userLocale));
return true;
} catch (final IOException e) {
e.printStackTrace();
diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java
index 6542541..9850aac 100644
--- a/src/main/java/sevenUnitsGUI/TabbedView.java
+++ b/src/main/java/sevenUnitsGUI/TabbedView.java
@@ -24,13 +24,16 @@ import java.awt.event.ItemEvent;
import java.awt.event.KeyEvent;
import java.util.AbstractSet;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
+import java.util.function.Consumer;
import java.util.function.Function;
import javax.swing.BorderFactory;
@@ -195,10 +198,14 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
/** The text box for prefix data in the prefix viewer */
private final JTextArea prefixTextBox;
- // SETTINGS STUFF
+ // INFO & SETTINGS STUFF
+ final JTextArea infoTextArea;
+ private final JComboBox<String> localeSelector;
private StandardRoundingType roundingType;
private int precision;
+ private final Map<String, Consumer<String>> localizedTextSetters;
+
/**
* Creates the view and makes it visible to the user
*
@@ -218,16 +225,20 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// initialize important components
this.presenter = new Presenter(this);
- this.frame = new JFrame("7Units " + ProgramInfo.VERSION);
+ this.frame = new JFrame("7Units (Unlocalized)");
this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
// master components (those that contain everything else within them)
this.masterPane = new JTabbedPane();
this.frame.add(this.masterPane);
+ this.localizedTextSetters = new HashMap<>();
+
// ============ UNIT CONVERSION TAB ============
final JPanel convertUnitPanel = new JPanel();
this.masterPane.addTab("Convert Units", convertUnitPanel);
+ this.localizedTextSetters.put("tv.convert_units.title",
+ txt -> this.masterPane.setTitleAt(0, txt));
this.masterPane.setMnemonicAt(0, KeyEvent.VK_U);
convertUnitPanel.setLayout(new BorderLayout());
@@ -263,7 +274,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
outputPanel.setLayout(new BorderLayout());
outputPanel.setBorder(new EmptyBorder(3, 6, 6, 6));
- final JLabel valuePrompt = new JLabel("Value to convert: ");
+ final JLabel valuePrompt = new JLabel();
+ this.localizedTextSetters.put("tv.convert_units.value_prompt",
+ valuePrompt::setText);
outputPanel.add(valuePrompt, BorderLayout.LINE_START);
this.valueInput = new JTextField();
@@ -271,6 +284,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// conversion button
this.convertUnitButton = new JButton("Convert");
+ this.localizedTextSetters.put("tv.convert_units.convert_btn",
+ this.convertUnitButton::setText);
outputPanel.add(this.convertUnitButton, BorderLayout.LINE_END);
this.convertUnitButton
.addActionListener(e -> this.presenter.convertUnits());
@@ -286,20 +301,26 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
final JPanel convertExpressionPanel = new JPanel();
this.masterPane.addTab("Convert Unit Expressions",
convertExpressionPanel);
+ this.localizedTextSetters.put("tv.convert_expressions.title",
+ txt -> this.masterPane.setTitleAt(1, txt));
this.masterPane.setMnemonicAt(1, KeyEvent.VK_E);
convertExpressionPanel.setLayout(new GridLayout(4, 1));
// from and to expressions
this.fromEntry = new JTextField();
convertExpressionPanel.add(this.fromEntry);
- this.fromEntry.setBorder(BorderFactory.createTitledBorder("From"));
+ this.localizedTextSetters.put("tv.convert_expressions.from",
+ txt -> this.fromEntry.setBorder(BorderFactory.createTitledBorder(txt)));
this.toEntry = new JTextField();
convertExpressionPanel.add(this.toEntry);
- this.toEntry.setBorder(BorderFactory.createTitledBorder("To"));
+ this.localizedTextSetters.put("tv.convert_expressions.to",
+ txt -> this.toEntry.setBorder(BorderFactory.createTitledBorder(txt)));
// button to convert
- this.convertExpressionButton = new JButton("Convert");
+ this.convertExpressionButton = new JButton();
+ this.localizedTextSetters.put("tv.convert_expressions.convert_btn",
+ this.convertExpressionButton::setText);
convertExpressionPanel.add(this.convertExpressionButton);
this.convertExpressionButton
@@ -309,13 +330,15 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// output of conversion
this.expressionOutput = new JTextArea(2, 32);
convertExpressionPanel.add(this.expressionOutput);
- this.expressionOutput
- .setBorder(BorderFactory.createTitledBorder("Output"));
+ this.localizedTextSetters.put("tv.convert_expressions.output",
+ txt -> this.expressionOutput.setBorder(BorderFactory.createTitledBorder(txt)));
this.expressionOutput.setEditable(false);
// =========== UNIT VIEWER ===========
final JPanel unitLookupPanel = new JPanel();
this.masterPane.addTab("Unit Viewer", unitLookupPanel);
+ this.localizedTextSetters.put("tv.unit_viewer.title",
+ txt -> this.masterPane.setTitleAt(2, txt));
this.masterPane.setMnemonicAt(2, KeyEvent.VK_V);
unitLookupPanel.setLayout(new GridLayout());
@@ -333,6 +356,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// ============ PREFIX VIEWER =============
final JPanel prefixLookupPanel = new JPanel();
this.masterPane.addTab("Prefix Viewer", prefixLookupPanel);
+ this.localizedTextSetters.put("tv.prefix_viewer.title",
+ txt -> this.masterPane.setTitleAt(3, txt));
this.masterPane.setMnemonicAt(3, KeyEvent.VK_P);
prefixLookupPanel.setLayout(new GridLayout(1, 2));
@@ -353,19 +378,20 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
this.masterPane.addTab("\uD83D\uDEC8", // info (i) character
new JScrollPane(infoPanel));
- final JTextArea infoTextArea = new JTextArea();
- infoTextArea.setEditable(false);
- infoTextArea.setOpaque(false);
- infoPanel.add(infoTextArea);
- infoTextArea.setText(this.presenter.getAboutText());
+ this.infoTextArea = new JTextArea();
+ this.infoTextArea.setEditable(false);
+ this.infoTextArea.setOpaque(false);
+ infoPanel.add(this.infoTextArea);
// ============ SETTINGS PANEL ============
+ this.localeSelector = new JComboBox<>();
this.masterPane.addTab("\u2699",
new JScrollPane(this.createSettingsPanel()));
this.masterPane.setMnemonicAt(5, KeyEvent.VK_S);
// ============ FINALIZE CREATION OF VIEW ============
this.presenter.postViewInitialize();
+ this.updateText();
this.frame.pack();
this.frame.setVisible(true);
}
@@ -386,7 +412,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
{
final JPanel roundingPanel = new JPanel();
settingsPanel.add(roundingPanel);
- roundingPanel.setBorder(new TitledBorder("Rounding Settings"));
+ this.localizedTextSetters.put("tv.settings.rounding.title",
+ txt -> roundingPanel.setBorder(new TitledBorder(txt)));
roundingPanel.setLayout(new GridBagLayout());
// rounding rule selection
@@ -396,13 +423,17 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
"Presenter loaded non-standard rounding rule"));
this.precision = this.getPresenterPrecision().orElse(6);
- final JLabel roundingRuleLabel = new JLabel("Rounding Rule:");
+ final JLabel roundingRuleLabel = new JLabel();
+ this.localizedTextSetters.put("tv.settings.rounding.rule",
+ roundingRuleLabel::setText);
roundingPanel.add(roundingRuleLabel, new GridBagBuilder(0, 0)
.setAnchor(GridBagConstraints.LINE_START).build());
// sigDigSlider needs to be first so that the rounding-type buttons can
// show and hide it
- final JLabel sliderLabel = new JLabel("Precision:");
+ final JLabel sliderLabel = new JLabel();
+ this.localizedTextSetters.put("tv.settings.rounding.precision",
+ sliderLabel::setText);
sliderLabel.setVisible(
this.roundingType != StandardRoundingType.UNCERTAINTY);
roundingPanel.add(sliderLabel, new GridBagBuilder(0, 4)
@@ -428,8 +459,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
});
// significant digit rounding
- final JRadioButton fixedPrecision = new JRadioButton(
- "Fixed Precision");
+ final JRadioButton fixedPrecision = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.rounding.fixed_sigfig",
+ fixedPrecision::setText);
if (this.roundingType == StandardRoundingType.SIGNIFICANT_DIGITS) {
fixedPrecision.setSelected(true);
}
@@ -444,8 +476,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
.setAnchor(GridBagConstraints.LINE_START).build());
// decimal place rounding
- final JRadioButton fixedDecimals = new JRadioButton(
- "Fixed Decimal Places");
+ final JRadioButton fixedDecimals = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.rounding.fixed_places",
+ fixedDecimals::setText);
if (this.roundingType == StandardRoundingType.DECIMAL_PLACES) {
fixedDecimals.setSelected(true);
}
@@ -460,8 +493,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
.setAnchor(GridBagConstraints.LINE_START).build());
// scientific rounding
- final JRadioButton relativePrecision = new JRadioButton(
- "Uncertainty-Based Rounding");
+ final JRadioButton relativePrecision = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.rounding.uncertainty",
+ relativePrecision::setText);
if (this.roundingType == StandardRoundingType.UNCERTAINTY) {
relativePrecision.setSelected(true);
}
@@ -480,8 +514,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
{
final JPanel prefixRepetitionPanel = new JPanel();
settingsPanel.add(prefixRepetitionPanel);
- prefixRepetitionPanel
- .setBorder(new TitledBorder("Prefix Repetition Settings"));
+ this.localizedTextSetters.put("tv.settings.repetition.title",
+ txt -> prefixRepetitionPanel.setBorder(new TitledBorder(txt)));
prefixRepetitionPanel.setLayout(new GridBagLayout());
final var prefixRule = this.getPresenterPrefixRule()
@@ -491,7 +525,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
// prefix rules
final ButtonGroup prefixRuleButtons = new ButtonGroup();
- final JRadioButton noRepetition = new JRadioButton("No Repetition");
+ final JRadioButton noRepetition = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.repetition.no",
+ noRepetition::setText);
if (prefixRule == DefaultPrefixRepetitionRule.NO_REPETITION) {
noRepetition.setSelected(true);
}
@@ -504,7 +540,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
prefixRepetitionPanel.add(noRepetition, new GridBagBuilder(0, 0)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JRadioButton noRestriction = new JRadioButton("No Restriction");
+ final JRadioButton noRestriction = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.repetition.any",
+ noRestriction::setText);
if (prefixRule == DefaultPrefixRepetitionRule.NO_RESTRICTION) {
noRestriction.setSelected(true);
}
@@ -517,8 +555,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
prefixRepetitionPanel.add(noRestriction, new GridBagBuilder(0, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JRadioButton customRepetition = new JRadioButton(
- "Complex Repetition");
+ final JRadioButton customRepetition = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.repetition.complex",
+ customRepetition::setText);
if (prefixRule == DefaultPrefixRepetitionRule.COMPLEX_REPETITION) {
customRepetition.setSelected(true);
}
@@ -536,7 +575,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
{
final JPanel searchingPanel = new JPanel();
settingsPanel.add(searchingPanel);
- searchingPanel.setBorder(new TitledBorder("Search Settings"));
+ this.localizedTextSetters.put("tv.settings.search.title",
+ txt -> searchingPanel.setBorder(new TitledBorder(txt)));
searchingPanel.setLayout(new GridBagLayout());
// searching rules
@@ -544,8 +584,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
final var searchRule = this.presenter.getSearchRule();
- final JRadioButton noPrefixes = new JRadioButton(
- "Never Include Prefixed Units");
+ final JRadioButton noPrefixes = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.search.no_prefixes",
+ noPrefixes::setText);
noPrefixes.addActionListener(e -> {
this.presenter.setSearchRule(PrefixSearchRule.NO_PREFIXES);
this.presenter.updateView();
@@ -555,8 +596,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
searchingPanel.add(noPrefixes, new GridBagBuilder(0, 0)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JRadioButton commonPrefixes = new JRadioButton(
- "Include Common Prefixes");
+ final JRadioButton commonPrefixes = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.search.common_prefixes",
+ commonPrefixes::setText);
commonPrefixes.addActionListener(e -> {
this.presenter.setSearchRule(PrefixSearchRule.COMMON_PREFIXES);
this.presenter.updateView();
@@ -566,8 +608,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
searchingPanel.add(commonPrefixes, new GridBagBuilder(0, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JRadioButton alwaysInclude = new JRadioButton(
- "Include All Single Prefixes");
+ final JRadioButton alwaysInclude = new JRadioButton();
+ this.localizedTextSetters.put("tv.settings.search.all_prefixes",
+ alwaysInclude::setText);
alwaysInclude.addActionListener(e -> {
this.presenter
.setSearchRule(this.presenter.getUniversalSearchRule());
@@ -596,30 +639,62 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
settingsPanel.add(miscPanel);
miscPanel.setLayout(new GridBagLayout());
- final JCheckBox oneWay = new JCheckBox("Convert One Way Only");
+ final JCheckBox oneWay = new JCheckBox();
+ this.localizedTextSetters.put("tv.settings.oneway", oneWay::setText);
oneWay.setSelected(this.presenter.oneWayConversionEnabled());
oneWay.addItemListener(e -> {
this.presenter.setOneWayConversionEnabled(
e.getStateChange() == ItemEvent.SELECTED);
this.presenter.saveSettings();
});
- miscPanel.add(oneWay, new GridBagBuilder(0, 0)
+ miscPanel.add(oneWay, new GridBagBuilder(0, 0, 2, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JCheckBox showAllVariations = new JCheckBox(
- "Show Duplicate Units & Prefixes");
+ final JCheckBox showAllVariations = new JCheckBox();
+ this.localizedTextSetters.put("tv.settings.show_duplicate",
+ showAllVariations::setText);
showAllVariations.setSelected(this.presenter.duplicatesShown());
showAllVariations.addItemListener(e -> {
this.presenter
.setShowDuplicates(e.getStateChange() == ItemEvent.SELECTED);
this.presenter.saveSettings();
});
- miscPanel.add(showAllVariations, new GridBagBuilder(0, 1)
+ miscPanel.add(showAllVariations, new GridBagBuilder(0, 1, 2, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ final JCheckBox useDefaultFiles = new JCheckBox();
+ this.localizedTextSetters.put("tv.settings.use_default_files",
+ useDefaultFiles::setText);
+ useDefaultFiles.setSelected(this.presenter.usingDefaultDatafiles());
+ useDefaultFiles.addItemListener(e -> {
+ this.presenter
+ .setUseDefaultDatafiles(e.getStateChange() == ItemEvent.SELECTED);
+ this.presenter.saveSettings();
+ });
+ miscPanel.add(useDefaultFiles, new GridBagBuilder(0, 2, 2, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
- final JButton unitFileButton = new JButton("Manage Unit Data Files");
+ final JLabel localeLabel = new JLabel();
+ this.localizedTextSetters.put("tv.settings.locale",
+ localeLabel::setText);
+ miscPanel.add(localeLabel, new GridBagBuilder(0, 3, 1, 1)
+ .setAnchor(GridBagConstraints.LINE_START).build());
+
+ this.presenter.getAvailableLocales().stream().sorted()
+ .forEachOrdered(this.localeSelector::addItem);
+ this.localeSelector.setSelectedItem(this.presenter.getUserLocale());
+ this.localeSelector.addItemListener(e -> {
+ this.presenter.setUserLocale((String) e.getItem());
+ this.presenter.saveSettings();
+ });
+ miscPanel.add(localeSelector, new GridBagBuilder(1, 3, 1, 1)
+ .setAnchor(GridBagConstraints.LINE_END).build());
+
+ final JButton unitFileButton = new JButton();
+ this.localizedTextSetters.put("tv.settings.unitfiles.button",
+ unitFileButton::setText);
unitFileButton.setEnabled(false);
- miscPanel.add(unitFileButton, new GridBagBuilder(0, 2)
+ miscPanel.add(unitFileButton, new GridBagBuilder(0, 4, 2, 1)
.setAnchor(GridBagConstraints.LINE_START).build());
}
@@ -827,4 +902,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView {
this.presenter.setNumberDisplayRule(roundingRule);
this.presenter.saveSettings();
}
+
+ @Override
+ public void updateText() {
+ this.frame.setTitle(this.presenter.getLocalizedText("tv.title")
+ .replace("[v]", ProgramInfo.VERSION.toString()));
+ this.infoTextArea.setText(this.presenter.getAboutText());
+ this.localizedTextSetters.forEach((id, action) ->
+ action.accept(this.presenter.getLocalizedText(id)));
+ }
}
diff --git a/src/main/java/sevenUnitsGUI/View.java b/src/main/java/sevenUnitsGUI/View.java
index 7dd0c44..4140992 100644
--- a/src/main/java/sevenUnitsGUI/View.java
+++ b/src/main/java/sevenUnitsGUI/View.java
@@ -112,4 +112,11 @@ public interface View {
*/
void showUnit(NameSymbol name, String definition, String dimensionName,
UnitType type);
+
+ /**
+ * Updates the view's text to reflect the presenter's locale.
+ *
+ * This method <b>must not</b> call {@link Presenter#setUserLocale(String)}.
+ */
+ void updateText();
}
diff --git a/src/main/java/sevenUnitsGUI/ViewBot.java b/src/main/java/sevenUnitsGUI/ViewBot.java
index e6593fb..8fff46d 100644
--- a/src/main/java/sevenUnitsGUI/ViewBot.java
+++ b/src/main/java/sevenUnitsGUI/ViewBot.java
@@ -505,4 +505,9 @@ public final class ViewBot
public List<UnitViewingRecord> unitViewList() {
return Collections.unmodifiableList(this.unitViewingRecords);
}
+
+ @Override
+ public void updateText() {
+ // do nothing, since ViewBot is not localized
+ }
}
diff --git a/src/main/resources/about.txt b/src/main/resources/about/en.txt
index 4c33f8c..068f922 100644
--- a/src/main/resources/about.txt
+++ b/src/main/resources/about/en.txt
@@ -15,7 +15,7 @@ Unit/Prefix/Dimension Statistics:
Copyright Notice:
-Unit Converter Copyright (C) 2018-2024 Adrien Hopkins
+Unit Converter Copyright (C) 2018-2025 Adrien Hopkins
This program comes with ABSOLUTELY NO WARRANTY;
for details read the LICENSE file, section 15
diff --git a/src/main/resources/about/fr.txt b/src/main/resources/about/fr.txt
new file mode 100644
index 0000000..d8d82aa
--- /dev/null
+++ b/src/main/resources/about/fr.txt
@@ -0,0 +1,24 @@
+À propos de 7Unités version [VERSION]
+
+7Unités est une programme pour convertir les unités avec plusieurs fonctions,
+inspiré par GNU Units (https://www.gnu.org/software/units/).
+Vous pouvez l’utiliser pour convertir des unités, mais vous pouvez aussi
+l’utiliser comme calculatrice, computer et convertir des expressions
+comme "10 m/s + (25^2 - 5^2) mi/hr".
+
+Ce logiciel est par Adrien Hopkins <adrien.p.hopkins@gmail.com>.
+
+Statistiques d’unités, préfixes et dimensions:
+
+[LOADSTATS]
+
+Copyright Notice:
+
+Unit Converter Copyright (C) 2018-2025 Adrien Hopkins
+This program comes with ABSOLUTELY NO WARRANTY;
+for details read the LICENSE file, section 15
+
+This is free software, and you are welcome to redistribute
+it under certain conditions; for details go to
+<https://www.gnu.org/licenses/quick-guide-gplv3.html>
+or read the LICENSE file.
diff --git a/src/main/resources/locales/en.txt b/src/main/resources/locales/en.txt
new file mode 100644
index 0000000..666e363
--- /dev/null
+++ b/src/main/resources/locales/en.txt
@@ -0,0 +1,31 @@
+tv.title=7Units [v]
+tv.convert_units.title=Convert Units
+tv.convert_units.value_prompt=Value to convert:
+tv.convert_units.convert_btn=Convert
+tv.convert_expressions.title=Convert Unit Expressions
+tv.convert_expressions.from=From
+tv.convert_expressions.to=To
+tv.convert_expressions.convert_btn=Convert
+tv.convert_expressions.output=Output
+tv.unit_viewer.title=Unit Viewer
+tv.prefix_viewer.title=Prefix Viewer
+tv.settings.rounding.title=Rounding Settings
+tv.settings.rounding.rule=Rounding Rule:
+tv.settings.rounding.precision=Precision:
+tv.settings.rounding.fixed_sigfig=Fixed Precision
+tv.settings.rounding.fixed_places=Fixed Decimal Places
+tv.settings.rounding.uncertainty=Uncertainty-based Rounding
+tv.settings.repetition.title=Prefix Repetition Settings
+tv.settings.repetition.no=No Repetition
+tv.settings.repetition.any=No Restriction
+tv.settings.repetition.complex=Complex Repetition
+tv.settings.search.title=Search Settings
+tv.settings.search.no_prefixes=Never Include Prefixed Units
+tv.settings.search.common_prefixes=Include Common Prefixes
+tv.settings.search.all_prefixes=Include All Single Prefixes
+tv.settings.oneway=Convert One Way Only
+tv.settings.show_duplicate=Show Duplicate Units & Prefixes
+tv.settings.use_default_files=Use Default Datafiles
+tv.settings.locale=🌐 Locale:
+tv.settings.unitfiles.button=Manage Unit Data Files
+load_stat_msg=Successfully loaded [u] unique units with [un] names ([b] base units), [p] unique prefixes with [pn] names, [s] unit sets, and [d] named dimensions.
diff --git a/src/main/resources/locales/fr.txt b/src/main/resources/locales/fr.txt
new file mode 100644
index 0000000..3fef030
--- /dev/null
+++ b/src/main/resources/locales/fr.txt
@@ -0,0 +1,31 @@
+tv.title=7Unités [v]
+tv.convert_units.title=Convertir Unités
+tv.convert_units.value_prompt=Valueur à convertir:
+tv.convert_units.convert_btn=Convertir
+tv.convert_expressions.title=Convertir Expressions
+tv.convert_expressions.from=De
+tv.convert_expressions.to=À
+tv.convert_expressions.convert_btn=Convertir
+tv.convert_expressions.output=Résultat
+tv.unit_viewer.title=Unités
+tv.prefix_viewer.title=Préfixes
+tv.settings.rounding.title=Préférences d’Arrondissement
+tv.settings.rounding.rule=Règle d’Arrondissement
+tv.settings.rounding.precision=Précision:
+tv.settings.rounding.fixed_sigfig=Precision fixe
+tv.settings.rounding.fixed_places=Chiffres fixe
+tv.settings.rounding.uncertainty=Arrondissement d’Incertitude
+tv.settings.repetition.title=Préférences de répetition de préfixes
+tv.settings.repetition.no=Non répetition
+tv.settings.repetition.any=Tout répetition
+tv.settings.repetition.complex=Répetition Complexe
+tv.settings.search.title=Préférences de Recherche
+tv.settings.search.no_prefixes=Jamais inclure unités préfixés
+tv.settings.search.common_prefixes=Inclure préfixes fréquents
+tv.settings.search.all_prefixes=Inclure tous préfixes seuls
+tv.settings.oneway=Convertir Seulement en un Direction
+tv.settings.show_duplicate=Montrer unités et préfixes doubles
+tv.settings.use_default_files=Utilise donées par défaut
+tv.settings.locale=🌐 Locale:
+tv.settings.unitfiles.button=Gérer donées d’unités
+load_stat_msg=Chargé [u] unités uniques avec [un] noms ([b] unités bases), [p] préfixes uniques avec [pn] noms, [s] collections d’unités, et [d] dimensions nomées.
diff --git a/src/test/java/sevenUnitsGUI/I18nTest.java b/src/test/java/sevenUnitsGUI/I18nTest.java
new file mode 100644
index 0000000..73bd727
--- /dev/null
+++ b/src/test/java/sevenUnitsGUI/I18nTest.java
@@ -0,0 +1,21 @@
+package sevenUnitsGUI;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.Test;
+
+class I18nTest {
+
+ /**
+ * Tests that the default locale exists.
+ *
+ * Currently this test fails.
+ */
+ @Test
+ void testDefaultLocale() {
+ Presenter p = new Presenter(new ViewBot());
+ assertNotNull(p.locales.get(Presenter.DEFAULT_LOCALE),
+ "Default locale does not exist.");
+ }
+
+}
diff --git a/src/test/java/sevenUnitsGUI/PresenterTest.java b/src/test/java/sevenUnitsGUI/PresenterTest.java
index 9e25a08..8b16365 100644
--- a/src/test/java/sevenUnitsGUI/PresenterTest.java
+++ b/src/test/java/sevenUnitsGUI/PresenterTest.java
@@ -17,6 +17,7 @@
package sevenUnitsGUI;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -154,6 +155,23 @@ public final class PresenterTest {
expectedOutput.getValue().toString(false, RoundingMode.HALF_EVEN));
assertEquals(List.of(expectedUC), viewBot.unitConversionList());
}
+
+ /**
+ * Ensures that the default unitfile can be disabled.
+ *
+ * @since v1.0.0
+ * @since 2025-02-23
+ */
+ @Test
+ void testDisableDefault() {
+ final var viewBot = new ViewBot();
+ final var presenter = new Presenter(viewBot);
+ assumeTrue(presenter.database.containsUnitName("joule"),
+ "Attempted to test disabling default on unit not in default file.");
+ presenter.setUseDefaultDatafiles(false);
+ assertFalse(presenter.database.containsUnitName("joule"),
+ "Presenter disabled default datafiles, but still contains the joule.");
+ }
/**
* Tests that duplicate units are successfully removed, if that is asked for