From 9a25a05d1b376dc20a14696afa280ff4940b1f8a Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Fri, 21 Feb 2025 23:51:13 -0500 Subject: Add internationalization API to GUI This commit intentionally fails one test, since that is for functionality I intend to add later. --- src/main/java/sevenUnitsGUI/Presenter.java | 58 +++++++++++++++++++++++++++++ src/main/java/sevenUnitsGUI/TabbedView.java | 5 +++ src/main/java/sevenUnitsGUI/View.java | 7 ++++ src/main/java/sevenUnitsGUI/ViewBot.java | 5 +++ 4 files changed, 75 insertions(+) (limited to 'src/main/java/sevenUnitsGUI') diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index c46ee53..21f2951 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -22,6 +22,7 @@ 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; @@ -77,6 +78,15 @@ public final class Presenter { /** A Predicate that returns true iff the argument is a full base unit */ private static final Predicate IS_FULL_BASE = unit -> unit instanceof LinearUnit && ((LinearUnit) unit).isBase(); + /** + * The default locale, used in two situations: + * + */ + static final String DEFAULT_LOCALE = "en"; /** * Adds default units and dimensions to a database. @@ -317,6 +327,12 @@ public final class Presenter { */ private final Set metricExceptions; + /** maps locale names (e.g. 'en') to key-text maps */ + final Map> 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 @@ -375,6 +391,9 @@ public final class Presenter { e); } + // TODO load locales + this.locales = new HashMap<>(); + // set default settings temporarily if (Files.exists(CONFIG_FILE)) { this.loadSettings(CONFIG_FILE); @@ -735,6 +754,14 @@ public final class Presenter { .replaceAll("\\[LOADSTATS\\]", wrapString(this.loadStatMsg(), 72)); } + /** + * @return set of all locales available to select + * @since 2025-02-21 + */ + public final Set getAvailableLocales() { + return this.locales.keySet(); + } + /** * Gets a name for this dimension using the database * @@ -750,6 +777,20 @@ public final class Presenter { .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 Map userLocale = this.locales.get(this.userLocale); + final Map 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 @@ -785,6 +826,14 @@ public final class Presenter { return this.searchRule; } + /** + * @return user's selected locale + * @since 2025-02-21 + */ + public String getUserLocale() { + return userLocale; + } + /** * @return a search rule that shows all single prefixes * @since 2022-07-08 @@ -1100,6 +1149,15 @@ public final class Presenter { 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(); + } + private List> settingsFromFile(Path settingsFile) { try (Stream lines = Files.lines(settingsFile)) { return lines.map(Presenter::withoutComments) diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 6542541..493fc10 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -827,4 +827,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.presenter.setNumberDisplayRule(roundingRule); this.presenter.saveSettings(); } + + @Override + public void updateText() { + // TODO Auto-generated method stub + } } 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 must not 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 unitViewList() { return Collections.unmodifiableList(this.unitViewingRecords); } + + @Override + public void updateText() { + // do nothing, since ViewBot is not localized + } } -- cgit v1.2.3 From d41c05feaaf543c473a9db7aa5a3e564cee0e4ed Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 22 Feb 2025 17:29:28 -0500 Subject: Load locales from text files --- src/main/java/sevenUnitsGUI/Presenter.java | 705 +++++++++++++++------------- src/main/java/sevenUnitsGUI/TabbedView.java | 3 +- src/main/resources/locales/en.txt | 1 + src/main/resources/locales/fr.txt | 1 + 4 files changed, 379 insertions(+), 331 deletions(-) create mode 100644 src/main/resources/locales/en.txt create mode 100644 src/main/resources/locales/fr.txt (limited to 'src/main/java/sevenUnitsGUI') diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 21f2951..1cbac47 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -16,7 +16,6 @@ */ package sevenUnitsGUI; -import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; @@ -58,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 */ @@ -78,19 +77,23 @@ public final class Presenter { /** A Predicate that returns true iff the argument is a full base unit */ private static final Predicate IS_FULL_BASE = unit -> unit instanceof LinearUnit && ((LinearUnit) unit).isBase(); - /** + /** * The default locale, used in two situations: *
    - *
  • If no text is available in your locale, - * uses text from this locale. - *
  • Users are initialized with this locale. + *
  • If no text is available in your locale, uses text from this locale. + *
  • Users are initialized with this locale. *
*/ - static final String DEFAULT_LOCALE = "en"; - + static final String DEFAULT_LOCALE = "eo"; + + private static final List 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 @@ -109,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 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) @@ -130,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). @@ -153,11 +156,11 @@ public final class Presenter { * @return contents of file * @since 2021-03-27 */ - private static final List getLinesFromResource(String filename) { + private static List getLinesFromResource(String filename) { final List 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()); } @@ -165,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. * @@ -176,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 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> 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"); @@ -281,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. Not implemented yet. */ private Function 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 @@ -305,50 +307,50 @@ public final class Presenter { */ private Function 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> 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> 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 metricExceptions; - + /** maps locale names (e.g. 'en') to key-text maps */ final Map> 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. */ private boolean showDuplicates = false; - + /** * Creates a Presenter - * + * * @param view the view that this presenter communicates with * @since 2021-12-15 */ @@ -356,30 +358,30 @@ public final class Presenter { this.view = view; this.database = new UnitDatabase(); addDefaults(this.database); - + // 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); @@ -390,19 +392,19 @@ public final class Presenter { throw new AssertionError("Loading of metric_exceptions.txt failed.", e); } - - // TODO load locales - this.locales = new HashMap<>(); - + + this.locales = this.loadLocales(); + this.userLocale = DEFAULT_LOCALE; + // 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. * @@ -410,23 +412,23 @@ public final class Presenter { * @return stream of entries, ready for flat-mapping * @since 2022-07-06 */ - private final Stream> applySearchRule( + private Stream> applySearchRule( Map.Entry 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 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 @@ -434,49 +436,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 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 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 convertExpressionToExpression( @@ -498,34 +497,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. @@ -542,27 +540,26 @@ public final class Presenter { "Could not recognize text in From entry: " + e.getMessage()); return Optional.empty(); } - + final List 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 toValues; try { toValues = from.convertToMultiple(toUnits); @@ -571,12 +568,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. @@ -593,8 +590,8 @@ public final class Presenter { "Could not recognize text in From entry: " + e.getMessage()); return Optional.empty(); } - - final List toUnits = this.database.getUnitSet(toName); + + final var toUnits = this.database.getUnitSet(toName); final List toValues; try { toValues = from.convertToMultiple(toUnits); @@ -603,16 +600,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 @@ -620,64 +617,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 fromUnitOptional = ucview.getFromSelection(); - final Optional 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 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 toMulti, UncertainDouble uncertainValue) { @@ -686,7 +680,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; @@ -695,46 +689,46 @@ public final class Presenter { initValue = UnitValue.of(fromUnit, uncertainValue.value()) .convertToBase(NameSymbol.EMPTY); } - - final List 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 @@ -742,26 +736,26 @@ public final class Presenter { public boolean duplicatesShown() { return this.showDuplicates; } - + /** * @return text in About file * @since 2022-02-19 */ - public final String getAboutText() { + public String getAboutText() { return Presenter.getLinesFromResource("/about.txt").stream() .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 final Set getAvailableLocales() { + public Set getAvailableLocales() { return this.locales.keySet(); } - + /** * Gets a name for this dimension using the database * @@ -769,28 +763,28 @@ public final class Presenter { * @return name of dimension * @since 2022-04-16 */ - final String getDimensionName(ObjectProduct dimension) { + String getDimensionName(ObjectProduct 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}. - * + * 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 Map userLocale = this.locales.get(this.userLocale); - final Map defaultLocale = this.locales.get(DEFAULT_LOCALE); + 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 @@ -799,7 +793,7 @@ public final class Presenter { public Function getNumberDisplayRule() { return this.numberDisplayRule; } - + /** * @return the rule that is used by this presenter to convert strings into * numbers @@ -809,7 +803,7 @@ public final class Presenter { private Function getNumberParsingRule() { return this.numberParsingRule; } - + /** * @return the rule that determines whether a set of prefixes is valid * @since 2022-04-19 @@ -817,7 +811,7 @@ public final class Presenter { public Predicate> getPrefixRepetitionRule() { return this.prefixRepetitionRule; } - + /** * @return the rule that determines which units are prefixed * @since 2022-07-08 @@ -825,15 +819,7 @@ public final class Presenter { public Function, Map> getSearchRule() { return this.searchRule; } - - /** - * @return user's selected locale - * @since 2025-02-21 - */ - public String getUserLocale() { - return userLocale; - } - + /** * @return a search rule that shows all single prefixes * @since 2022-07-08 @@ -842,7 +828,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 @@ -850,7 +844,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. @@ -859,7 +853,7 @@ public final class Presenter { */ private void handleLoadErrors(List 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"))); @@ -868,13 +862,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(); @@ -884,7 +878,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 @@ -893,18 +887,17 @@ public final class Presenter { * * @since 2024-08-16 */ - private final String linearUnitValueSumToString( - List values) { - final String integerPart = values.subList(0, values.size() - 1).stream() + private String linearUnitValueSumToString(List 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 lines = Files.lines(exceptionFile)) { + try (var lines = Files.lines(exceptionFile)) { lines.map(Presenter::withoutComments) .forEach(this.metricExceptions::add); } catch (final IOException e) { @@ -913,7 +906,54 @@ public final class Presenter { + exceptionFile + "\": " + e.getLocalizedMessage()); } } + + /** + * Loads all available locales, including custom ones, into a map. + * @return map containing locales + */ + private Map> loadLocales() { + final Map> locales = new HashMap<>(); + for (final String localeName : LOCAL_LOCALES) { + final Map 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> locales, Path file) throws IOException { + final Map 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 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. @@ -924,8 +964,8 @@ public final class Presenter { void loadSettings(Path settingsFile) { for (final Map.Entry 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 @@ -949,26 +989,29 @@ 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.setUserLocale(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 @@ -985,37 +1028,37 @@ public final class Presenter { this.database.unitSetMap().size(), 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(); } - + void prefixSelected() { - final Optional selectedPrefixName = this.view + final var selectedPrefixName = this.view .getViewedPrefixName(); final Optional selectedPrefix = selectedPrefixName .map(name -> this.database.containsPrefixName(name) @@ -1025,16 +1068,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); @@ -1042,19 +1085,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": @@ -1066,7 +1109,7 @@ public final class Presenter { break; } } - + /** * @param numberDisplayRule the new rule that will be used by this presenter * to convert numbers into strings @@ -1076,7 +1119,7 @@ public final class Presenter { Function numberDisplayRule) { this.numberDisplayRule = numberDisplayRule; } - + /** * @param numberParsingRule the new rule that will be used by this presenter * to convert strings into numbers @@ -1087,7 +1130,7 @@ public final class Presenter { Function numberParsingRule) { this.numberParsingRule = numberParsingRule; } - + /** * @param oneWayConversionEnabled whether not one-way conversion should be * enabled @@ -1098,7 +1141,7 @@ public final class Presenter { this.oneWayConversionEnabled = oneWayConversionEnabled; this.updateView(); } - + /** * @param prefixRepetitionRule the rule that determines whether a set of * prefixes is valid @@ -1109,7 +1152,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 @@ -1121,7 +1164,7 @@ public final class Presenter { Function, Map> searchRule) { this.searchRule = searchRule; } - + private void setSearchRuleFromString(String ruleString) { switch (ruleString) { case "NO_PREFIXES": @@ -1139,7 +1182,7 @@ public final class Presenter { ruleString); } } - + /** * @param showDuplicateUnits whether or not duplicate units should be shown * @since 2022-03-30 @@ -1148,18 +1191,9 @@ public final class Presenter { this.showDuplicates = showDuplicateUnits; 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(); - } - + private List> settingsFromFile(Path settingsFile) { - try (Stream lines = Files.lines(settingsFile)) { + try (var lines = Files.lines(settingsFile)) { return lines.map(Presenter::withoutComments) .filter(line -> !line.isBlank()).map(Presenter::parseSettingLine) .toList(); @@ -1169,7 +1203,17 @@ public final class Presenter { return null; } } - + + /** + * 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 * @@ -1178,53 +1222,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 selectedUnitName = this.view.getViewedUnitName(); + final var selectedUnitName = this.view.getViewedUnitName(); final Optional 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 @@ -1236,7 +1280,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(), @@ -1247,7 +1291,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())); @@ -1259,7 +1303,7 @@ public final class Presenter { ucview.setToUnitNames(toNames); } } - + /** * @param message message to add * @param args string formatting arguments for message @@ -1271,15 +1315,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( @@ -1290,6 +1334,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 493fc10..cb0a4d2 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -218,7 +218,8 @@ 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(this.presenter.getLocalizedText("tv.title") + .replace("[v]", ProgramInfo.VERSION.toString())); this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // master components (those that contain everything else within them) diff --git a/src/main/resources/locales/en.txt b/src/main/resources/locales/en.txt new file mode 100644 index 0000000..9d453c9 --- /dev/null +++ b/src/main/resources/locales/en.txt @@ -0,0 +1 @@ +tv.title=7Units [v] \ No newline at end of file diff --git a/src/main/resources/locales/fr.txt b/src/main/resources/locales/fr.txt new file mode 100644 index 0000000..1d08fda --- /dev/null +++ b/src/main/resources/locales/fr.txt @@ -0,0 +1 @@ +tv.title=7Unités [v] \ No newline at end of file -- cgit v1.2.3 From f1ca636346f4fe4da66035ae30469f1503261a83 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sat, 22 Feb 2025 17:48:23 -0500 Subject: Add ability for user to change locale --- src/main/java/sevenUnitsGUI/Presenter.java | 5 +++-- src/main/java/sevenUnitsGUI/TabbedView.java | 29 +++++++++++++++++++++++------ 2 files changed, 26 insertions(+), 8 deletions(-) (limited to 'src/main/java/sevenUnitsGUI') diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 1cbac47..6467f03 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -84,7 +84,7 @@ public final class Presenter { *
  • Users are initialized with this locale. * */ - static final String DEFAULT_LOCALE = "eo"; + static final String DEFAULT_LOCALE = "en"; private static final List LOCAL_LOCALES = List.of("en", "fr"); private static final Path USER_LOCALES_DIR = userConfigDir() @@ -998,7 +998,7 @@ public final class Presenter { this.setSearchRuleFromString(value); break; case "locale": - this.setUserLocale(value); + this.userLocale = value; break; default: System.err.printf("Warning: unrecognized setting \"%s\".%n", @@ -1055,6 +1055,7 @@ public final class Presenter { } this.updateView(); + this.view.updateText(); } void prefixSelected() { diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index cb0a4d2..1da119e 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -196,6 +196,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { private final JTextArea prefixTextBox; // SETTINGS STUFF + private final JComboBox localeSelector; private StandardRoundingType roundingType; private int precision; @@ -218,8 +219,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { // initialize important components this.presenter = new Presenter(this); - this.frame = new JFrame(this.presenter.getLocalizedText("tv.title") - .replace("[v]", ProgramInfo.VERSION.toString())); + this.frame = new JFrame("7Units (Unlocalized)"); this.frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); // master components (those that contain everything else within them) @@ -361,12 +361,14 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { infoTextArea.setText(this.presenter.getAboutText()); // ============ 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); } @@ -604,7 +606,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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( @@ -615,12 +617,26 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { .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 JLabel localeLabel = new JLabel("Locale:"); + miscPanel.add(localeLabel, new GridBagBuilder(0, 2, 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, 2, 1, 1) + .setAnchor(GridBagConstraints.LINE_END).build()); + final JButton unitFileButton = new JButton("Manage Unit Data Files"); unitFileButton.setEnabled(false); - miscPanel.add(unitFileButton, new GridBagBuilder(0, 2) + miscPanel.add(unitFileButton, new GridBagBuilder(0, 3, 2, 1) .setAnchor(GridBagConstraints.LINE_START).build()); } @@ -831,6 +847,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { @Override public void updateText() { - // TODO Auto-generated method stub + this.frame.setTitle(this.presenter.getLocalizedText("tv.title") + .replace("[v]", ProgramInfo.VERSION.toString())); } } -- cgit v1.2.3 From a62fd9e6b4519ffcbd0503c45b159207ce438243 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 23 Feb 2025 17:48:55 -0500 Subject: Localize all user-facing strings --- src/main/java/sevenUnitsGUI/TabbedView.java | 114 ++++++++++++++++++++-------- src/main/resources/locales/en.txt | 30 +++++++- 2 files changed, 110 insertions(+), 34 deletions(-) (limited to 'src/main/java/sevenUnitsGUI') diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 1da119e..ca9f23c 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; @@ -200,6 +203,8 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { private StandardRoundingType roundingType; private int precision; + private final Map> localizedTextSetters; + /** * Creates the view and makes it visible to the user * @@ -226,9 +231,13 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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()); @@ -264,7 +273,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(); @@ -272,6 +283,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()); @@ -287,20 +300,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 @@ -310,13 +329,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()); @@ -334,6 +355,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)); @@ -389,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 @@ -399,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) @@ -431,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); } @@ -447,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); } @@ -463,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); } @@ -483,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() @@ -494,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); } @@ -507,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); } @@ -520,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); } @@ -539,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 @@ -547,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(); @@ -558,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(); @@ -569,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()); @@ -599,7 +639,8 @@ 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( @@ -609,8 +650,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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 @@ -620,7 +662,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { miscPanel.add(showAllVariations, new GridBagBuilder(0, 1, 2, 1) .setAnchor(GridBagConstraints.LINE_START).build()); - final JLabel localeLabel = new JLabel("Locale:"); + final JLabel localeLabel = new JLabel(); + this.localizedTextSetters.put("tv.settings.locale", + localeLabel::setText); miscPanel.add(localeLabel, new GridBagBuilder(0, 2, 1, 1) .setAnchor(GridBagConstraints.LINE_START).build()); @@ -634,7 +678,9 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { miscPanel.add(localeSelector, new GridBagBuilder(1, 2, 1, 1) .setAnchor(GridBagConstraints.LINE_END).build()); - final JButton unitFileButton = new JButton("Manage Unit Data Files"); + final JButton unitFileButton = new JButton(); + this.localizedTextSetters.put("tv.settings.unitfiles.button", + unitFileButton::setText); unitFileButton.setEnabled(false); miscPanel.add(unitFileButton, new GridBagBuilder(0, 3, 2, 1) .setAnchor(GridBagConstraints.LINE_START).build()); @@ -849,5 +895,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { public void updateText() { this.frame.setTitle(this.presenter.getLocalizedText("tv.title") .replace("[v]", ProgramInfo.VERSION.toString())); + this.localizedTextSetters.forEach((id, action) -> + action.accept(this.presenter.getLocalizedText(id))); } } diff --git a/src/main/resources/locales/en.txt b/src/main/resources/locales/en.txt index 9d453c9..19ed781 100644 --- a/src/main/resources/locales/en.txt +++ b/src/main/resources/locales/en.txt @@ -1 +1,29 @@ -tv.title=7Units [v] \ No newline at end of file +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.locale=🌐 Locale: +tv.settings.unitfiles.button=Manage Unit Data Files -- cgit v1.2.3 From 4436b29053a0b757562ecc1d0a78e22902e6e5ae Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 23 Feb 2025 19:20:30 -0500 Subject: Allow default datafile to be disabled If this option is deselected, the default unit, prefix, dimension and metric exception data will not be loaded, and only custom data and the few units that are not provided by files will be available. The main rationale for this change is so that the data can be localized by custom unit files. --- src/main/java/sevenUnitsGUI/Presenter.java | 93 +++++++++++++++++++++----- src/main/java/sevenUnitsGUI/TabbedView.java | 18 ++++- src/main/resources/locales/en.txt | 1 + src/main/resources/locales/fr.txt | 1 + src/test/java/sevenUnitsGUI/PresenterTest.java | 18 +++++ 5 files changed, 110 insertions(+), 21 deletions(-) (limited to 'src/main/java/sevenUnitsGUI') diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index 6467f03..ba600e3 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -347,6 +347,19 @@ public final class Presenter { * unit view in views that show units as a list to choose from. */ 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 customUnitFiles = new HashSet<>(); + /** Custom dimension datafiles that will be loaded by {@link #reloadData} */ + private final Set customDimensionFiles = new HashSet<>(); + /** Custom exception datafiles that will be loaded by {@link #reloadData} */ + private final Set customExceptionFiles = new HashSet<>(); /** * Creates a Presenter @@ -357,8 +370,45 @@ public final class Presenter { public Presenter(View view) { this.view = view; this.database = new UnitDatabase(); - addDefaults(this.database); + 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 var units = inputStream(DEFAULT_UNITS_FILEPATH)) { this.handleLoadErrors(this.database.loadUnitsFromStream(units)); @@ -377,7 +427,6 @@ public final class Presenter { // load metric exceptions try { - this.metricExceptions = new HashSet<>(); try (var exceptions = inputStream(DEFAULT_EXCEPTIONS_FILEPATH); var scanner = new Scanner(exceptions)) { while (scanner.hasNextLine()) { @@ -392,17 +441,6 @@ public final class Presenter { throw new AssertionError("Loading of metric_exceptions.txt failed.", e); } - - this.locales = this.loadLocales(); - this.userLocale = DEFAULT_LOCALE; - - // set default settings temporarily - if (Files.exists(CONFIG_FILE)) { - this.loadSettings(CONFIG_FILE); - } - - // print out unit counts - System.out.println(this.loadStatMsg()); } /** @@ -962,6 +1000,10 @@ 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 setting : this .settingsFromFile(settingsFile)) { final var value = setting.getValue(); @@ -970,15 +1012,13 @@ public final class Presenter { // 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); @@ -1204,6 +1244,16 @@ public final class Presenter { return null; } } + + /** + * 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. @@ -1304,6 +1354,13 @@ public final class Presenter { ucview.setToUnitNames(toNames); } } + + /** + * @return true iff the default datafiles are being used + */ + public boolean usingDefaultDatafiles() { + return this.useDefaultDatafiles; + } /** * @param message message to add diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index ca9f23c..40ed0a7 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -662,10 +662,22 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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 JLabel localeLabel = new JLabel(); this.localizedTextSetters.put("tv.settings.locale", localeLabel::setText); - miscPanel.add(localeLabel, new GridBagBuilder(0, 2, 1, 1) + miscPanel.add(localeLabel, new GridBagBuilder(0, 3, 1, 1) .setAnchor(GridBagConstraints.LINE_START).build()); this.presenter.getAvailableLocales().stream().sorted() @@ -675,14 +687,14 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { this.presenter.setUserLocale((String) e.getItem()); this.presenter.saveSettings(); }); - miscPanel.add(localeSelector, new GridBagBuilder(1, 2, 1, 1) + 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, 3, 2, 1) + miscPanel.add(unitFileButton, new GridBagBuilder(0, 4, 2, 1) .setAnchor(GridBagConstraints.LINE_START).build()); } diff --git a/src/main/resources/locales/en.txt b/src/main/resources/locales/en.txt index 19ed781..5173cf3 100644 --- a/src/main/resources/locales/en.txt +++ b/src/main/resources/locales/en.txt @@ -25,5 +25,6 @@ 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 diff --git a/src/main/resources/locales/fr.txt b/src/main/resources/locales/fr.txt index d25e2b0..e8b7138 100644 --- a/src/main/resources/locales/fr.txt +++ b/src/main/resources/locales/fr.txt @@ -25,5 +25,6 @@ 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 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 -- cgit v1.2.3 From 9c358d708ba4988648d7b19ccb842f076ec4c354 Mon Sep 17 00:00:00 2001 From: Adrien Hopkins Date: Sun, 23 Feb 2025 20:23:47 -0500 Subject: Allow internationalization of about.txt This works with custom locales (by placing the text in [config_dir]/about/[name].txt), but if such a file does not exist, it will default to the default locale (en)'s about text. --- src/main/java/sevenUnitsGUI/Presenter.java | 43 +++++++++++++++++++++-------- src/main/java/sevenUnitsGUI/TabbedView.java | 13 +++++---- src/main/resources/about.txt | 25 ----------------- src/main/resources/about/en.txt | 25 +++++++++++++++++ src/main/resources/about/fr.txt | 24 ++++++++++++++++ src/main/resources/locales/en.txt | 1 + src/main/resources/locales/fr.txt | 1 + 7 files changed, 90 insertions(+), 42 deletions(-) delete mode 100644 src/main/resources/about.txt create mode 100644 src/main/resources/about/en.txt create mode 100644 src/main/resources/about/fr.txt (limited to 'src/main/java/sevenUnitsGUI') diff --git a/src/main/java/sevenUnitsGUI/Presenter.java b/src/main/java/sevenUnitsGUI/Presenter.java index ba600e3..3a039a7 100644 --- a/src/main/java/sevenUnitsGUI/Presenter.java +++ b/src/main/java/sevenUnitsGUI/Presenter.java @@ -780,7 +780,27 @@ public final class Presenter { * @since 2022-02-19 */ public String getAboutText() { - return Presenter.getLinesFromResource("/about.txt").stream() + 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 rawLines) { + return rawLines .map(Presenter::withoutComments).collect(Collectors.joining("\n")) .replaceAll("\\[VERSION\\]", ProgramInfo.VERSION.toString()) .replaceAll("\\[LOADSTATS\\]", wrapString(this.loadStatMsg(), 72)); @@ -1057,16 +1077,17 @@ public final class Presenter { * @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())); } /** diff --git a/src/main/java/sevenUnitsGUI/TabbedView.java b/src/main/java/sevenUnitsGUI/TabbedView.java index 40ed0a7..9850aac 100644 --- a/src/main/java/sevenUnitsGUI/TabbedView.java +++ b/src/main/java/sevenUnitsGUI/TabbedView.java @@ -198,7 +198,8 @@ 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 localeSelector; private StandardRoundingType roundingType; private int precision; @@ -377,11 +378,10 @@ 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<>(); @@ -907,6 +907,7 @@ final class TabbedView implements ExpressionConversionView, UnitConversionView { 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/resources/about.txt b/src/main/resources/about.txt deleted file mode 100644 index 4c33f8c..0000000 --- a/src/main/resources/about.txt +++ /dev/null @@ -1,25 +0,0 @@ -About 7Units Version [VERSION] - -7Units is a unit converter program with many features, -inspired by GNU Units (https://www.gnu.org/software/units/). -You can use it to simply convert units, but you can also -use it like a calculator, computing and converting expressions -like "10 m/s + (25^2 - 5^2) mi/hr". - -This software was written by Adrien Hopkins -. - -Unit/Prefix/Dimension Statistics: - -[LOADSTATS] - -Copyright Notice: - -Unit Converter Copyright (C) 2018-2024 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 - -or read the LICENSE file. diff --git a/src/main/resources/about/en.txt b/src/main/resources/about/en.txt new file mode 100644 index 0000000..068f922 --- /dev/null +++ b/src/main/resources/about/en.txt @@ -0,0 +1,25 @@ +About 7Units Version [VERSION] + +7Units is a unit converter program with many features, +inspired by GNU Units (https://www.gnu.org/software/units/). +You can use it to simply convert units, but you can also +use it like a calculator, computing and converting expressions +like "10 m/s + (25^2 - 5^2) mi/hr". + +This software was written by Adrien Hopkins +. + +Unit/Prefix/Dimension Statistics: + +[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 + +or read the LICENSE file. 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 . + +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 + +or read the LICENSE file. diff --git a/src/main/resources/locales/en.txt b/src/main/resources/locales/en.txt index 5173cf3..666e363 100644 --- a/src/main/resources/locales/en.txt +++ b/src/main/resources/locales/en.txt @@ -28,3 +28,4 @@ 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 index e8b7138..3fef030 100644 --- a/src/main/resources/locales/fr.txt +++ b/src/main/resources/locales/fr.txt @@ -28,3 +28,4 @@ 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. -- cgit v1.2.3