All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sirius.kernel.nls.NLS Maven / Gradle / Ivy

Go to download

Provides common core classes and the microkernel powering all Sirius applications

There is a newer version: 12.9.1
Show newest version
/*
 * Made with all the love in the world
 * by scireum in Remshalden, Germany
 *
 * Copyright by scireum GmbH
 * http://www.scireum.de - [email protected]
 */

package sirius.kernel.nls;

import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import sirius.kernel.Classpath;
import sirius.kernel.Sirius;
import sirius.kernel.async.CallContext;
import sirius.kernel.async.ExecutionPoint;
import sirius.kernel.commons.AdvancedDateParser;
import sirius.kernel.commons.Amount;
import sirius.kernel.commons.Explain;
import sirius.kernel.commons.Lambdas;
import sirius.kernel.commons.NumberFormat;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Value;
import sirius.kernel.di.Injector;
import sirius.kernel.health.Exceptions;
import sirius.kernel.timer.Timers;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.net.URL;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.Calendar;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

/**
 * Native Language Support used by the framework.
 * 

* This class provides a translation engine ({@link #get(String)}, {@link #safeGet(String, String, String)}, * {@link #fmtr(String)}). It also provides access to the current language via {@link #getCurrentLang()} and to the * default language ({@link #getDefaultLanguage()}. Most of the methods come in two versions, one which accepts a * lang parameter and another which uses the currently active language. *

* Additionally this class provides conversion methods to and from String. The most prominent ones are * {@link #toUserString(Object)} and {@link #toMachineString(Object)} along with their equivalent parse methods. * Although some conversions, especially toMachineString or formatSize are not language dependent, * those are kept in this class, to keep all conversion methods together. *

* Babelfish is used as translation engine and responsible for loading all provided .properties files. *

* Configuration *

    *
  • nls.defaultLanguage: Sets the two-letter code used as default language
  • *
  • nls.language: Sets an array of two-letter codes which enumerate all supported languages
  • *
* * @see Babelfish */ @SuppressWarnings("squid:S1192") @Explain("String literales here have different semantics and are therefore duplicated.") public class NLS { private static final Babelfish blubb = new Babelfish(); private static String defaultLanguage; private static Set supportedLanguages; private static final DateTimeFormatter MACHINE_DATE_TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH); private static final DateTimeFormatter MACHINE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ENGLISH); private static final DateTimeFormatter MACHINE_PARSE_TIME_FORMAT = DateTimeFormatter.ofPattern("H:mm[:ss]", Locale.ENGLISH); private static final DateTimeFormatter MACHINE_FORMAT_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss", Locale.ENGLISH); private static final Map dateTimeFormatters = Maps.newTreeMap(); private static final Map dateFormatters = Maps.newTreeMap(); private static final Map shortDateFormatters = Maps.newTreeMap(); private static final Map timeFormatters = Maps.newTreeMap(); private static final Map parseTimeFormatters = Maps.newTreeMap(); private static final Map fullTimeFormatters = Maps.newTreeMap(); private static final long SECOND = 1000; private static final long MINUTE = 60 * SECOND; private static final long HOUR = 60 * MINUTE; private static final long DAY = 24 * HOUR; private static final String[] UNITS = {"Bytes", "kB", "MB", "GB", "TB", "PB"}; private NLS() { } /** * Returns the currently active language as two-letter code. * * @return a two-letter code of the currently active language, as defined in * {@link sirius.kernel.async.CallContext#getLang()} */ @Nonnull public static String getCurrentLang() { return CallContext.getCurrent().getLang(); } /** * Returns the two-letter code of the default language. Provided via the config in {@code nls.defaultLanguage} *

* If this is set to "auto" the default language will be the system language. * * @return the language code of the default language */ @Nonnull public static String getDefaultLanguage() { if (defaultLanguage != null) { return defaultLanguage; } return determineDefaultLanguage(); } private static String determineDefaultLanguage() { if (defaultLanguage == null && Sirius.getSettings() != null) { defaultLanguage = Sirius.getSettings().getString("nls.defaultLanguage").toLowerCase(); if ("auto".equals(defaultLanguage)) { defaultLanguage = getSystemLanguage(); } } // Returns the default language or (for very early access we default to en) return defaultLanguage == null ? "en" : defaultLanguage; } /** * Returns the two-letter code of the fall back language. Provided via the {@link CallContext}. If the value is * empty, {@link NLS#getDefaultLanguage} is returned. * * @return the language code of the fallback language */ @Nonnull public static String getFallbackLanguage() { String fallback = CallContext.getCurrent().getFallbackLang(); if (Strings.isEmpty(fallback)) { return getDefaultLanguage(); } return fallback; } /** * Overrides the default language as defined in the configuration ({@code nls.defaultLanguage}). *

* This can be used to enforce the system language. However, setting nls.defaultLanguage to 'auto' is * the recommended approach. * * @param lang the two letter language code to use as default language. * @see #getSystemLanguage() */ public static void setDefaultLanguage(String lang) { defaultLanguage = lang; } /** * Returns the two-letter code of the system language. *

* By default, SIRIUS initializes with the language set in {@code nls.defaultLanguage} so a switchover * to the system language has to be performed manually. * * @return the language code of the underlying operating system. If the language is not supported (not listed * in {@code nls.languages}), null will be returned as * {@link sirius.kernel.async.CallContext#setLang(String)} doesn't change the current language if null is * passed in. */ @Nullable public static String getSystemLanguage() { return makeLang(Locale.getDefault().getLanguage().toLowerCase()); } /** * Returns a list of two-letter codes enumerating all supported languages. Provided via the config in * {@code nls.languages} * * @return a list of supported language codes */ public static Set getSupportedLanguages() { if (supportedLanguages == null && Sirius.getSettings() != null) { try { supportedLanguages = Sirius.getSettings() .getStringList("nls.languages") .stream() .map(String::toLowerCase) .collect(Lambdas.into(Sets.newLinkedHashSet())); } catch (Exception e) { Exceptions.handle(e); } } // Returns the default language or (for very early access we default to en) return supportedLanguages == null ? Collections.singleton("en") : Collections.unmodifiableSet(supportedLanguages); } /** * Determines if the given language code is supported or not. * * @param twoLetterLanguageCode the language as two-letter code * @return true if the language is listed in nls.languages, false otherwise. */ public static boolean isSupportedLanguage(String twoLetterLanguageCode) { return getSupportedLanguages().contains(twoLetterLanguageCode); } /** * Checks if the given language is supproted. Returns the default language otherwise. *

* Note that if the given lang is empty or null, this method will also return null as a call * to {@link sirius.kernel.async.CallContext#setLang(String)} with null as parameter won't change * the language at all. * * @param lang the language to check * @return lang if it was a supported language or the defaultLanguage otherwise, unless an empty string * was passed in, in which case null is returned. */ @Nullable public static String makeLang(@Nullable String lang) { if (Strings.isEmpty(lang)) { return null; } String langAsLowerCase = lang.toLowerCase(); if (getSupportedLanguages().contains(langAsLowerCase)) { return langAsLowerCase; } else { return getDefaultLanguage(); } } /** * Initializes the engine based on the given classpath * * @param classpath the classpath used to discover all .properties files */ public static void init(Classpath classpath) { blubb.init(classpath); } /** * Start the monitoring of resources in development environments. * * @param classpath the classpath used to resolve .properties files */ public static void startMonitoring(Classpath classpath) { Timers timer = Injector.context().getPart(Timers.class); for (String name : blubb.getLoadedResourceBundles()) { URL resource = classpath.getLoader().getResource(name); if ("file".equals(resource.getProtocol()) && !resource.getFile().contains("!")) { timer.addWatchedResource(resource, () -> blubb.reloadBundle(name)); } } } /** * Provides direct access to the translation engine to supply new properties or inspect current ones. * * @return the internally used translation engine */ public static Babelfish getTranslationEngine() { return blubb; } /** * Returns a translated text for the given property and the currently active language. *

* If no translation is found, the translation for the default language is used. If still no translation is * found, the property itself is returned. * * @param property the key for which a translation is requested * @return a translated string for the current language (or for the default language if no translation was found) * or the property itself if no translation for neither of both languages is available. */ public static String get(@Nonnull String property) { return blubb.get(property, null, true).translate(getCurrentLang(), getFallbackLanguage()); } /** * Returns a translated text for the given property in the given language. *

* The same fallback rules as for {@link #get(String)} apply. * * @param property the key for which a translation is requested * @param lang a two-letter language code for which the translation is requested * @return a translated string in the requested language or a fallback value if no translation was found */ @SuppressWarnings("squid:S2637") @Explain("Strings.isEmpty checks for null on lang") public static String get(@Nonnull String property, @Nullable String lang) { return blubb.get(property, null, true) .translate(Strings.isEmpty(lang) ? getCurrentLang() : lang, getFallbackLanguage()); } /** * Returns one of two or three versions of a translation based on the given numeric for the current language. * * @param property the property to fetch * @param numeric the numeric used to determine which version to use * @return the version of the given property in the current language based on the given numeric * @see #get(String, int, String) */ public static String get(@Nonnull String property, int numeric) { return get(property, numeric, null); } /** * Returns one of two or three versions of a translation based on the given numeric. *

* The property has to be defined like: *

     * property.key=Version for 0|Version for 1|Version for many
     * 
*

* Alternatively, only two versions can be given: *

     * property.key=Version for 1|Version for 0 or many
     * 
*

* Based on the given numeric the right version will be chosen. * * @param property the property to fetch * @param numeric the numeric used to determine which version to use * @param lang the language to translate for * @return the version of the given property in the given language based on the given numeric */ public static String get(@Nonnull String property, int numeric, @Nullable String lang) { String value = get(property, lang); String[] versions = value.split("\\|"); if (versions.length == 1) { Babelfish.LOG.WARN( "A numeric translation was accessed which doesn't provide any versions: %s, Lang: %s, Value: %s\n%s", property, lang, value, ExecutionPoint.snapshot()); return value; } if (numeric == 0) { if (versions.length == 3) { return versions[0].trim(); } else { return Formatter.create(versions[1].trim()).set("count", 0).format(); } } else if (numeric == 1) { if (versions.length == 3) { return versions[1].trim(); } else { return versions[0].trim(); } } else { if (versions.length == 3) { return Formatter.create(versions[2].trim()).set("count", numeric).format(); } else { return Formatter.create(versions[1].trim()).set("count", numeric).format(); } } } /** * Returns a translated text for the given property in the given language * or null if no translation was found. *

* The same fallback rules as for {@link #get(String, String)} apply. However, if no translation * * @param property the key for which a translation is requested * @param lang a two-letter language code for which the translation is requested * @return a translated text in the requested language (or in the default language if no translation for the given * language was found). Returns an empty optional if no translation for this property exists at all. */ @SuppressWarnings("squid:S2637") @Explain("Strings.isEmpty checks for null on lang") public static Optional getIfExists(@Nonnull String property, @Nullable String lang) { if (Strings.isEmpty(property)) { return Optional.empty(); } Translation translation = blubb.get(property, null, false); if (translation == null) { return Optional.empty(); } return Optional.of(translation.translate(Strings.isEmpty(lang) ? getCurrentLang() : lang, getFallbackLanguage())); } /** * Returns a translated text for the given property or for the given fallback, if no translation * for property was found. * * @param property the primary key for which a translation is requested * @param fallback the fallback key for which a translation is requested * @param lang a two-letter language code for which the translation is requested * @return a translated text in the requested language for the given property, or for the given fallback. If either * of both doesn't provide a translation for the given language, the translation for the default * language is returned. If neither of both keys exist property will be returned. */ @SuppressWarnings("squid:S2637") @Explain("Strings.isEmpty checks for null on lang") public static String safeGet(@Nonnull String property, @Nonnull String fallback, @Nullable String lang) { return blubb.get(property, fallback, true) .translate(Strings.isEmpty(lang) ? getCurrentLang() : lang, getFallbackLanguage()); } /** * Returns a translated text for the given property or for the given fallback, if no translation * for property was found. * * @param property the primary key for which a translation is requested * @param fallback the fallback key for which a translation is requested * @return a translated text in the current language for the given property, or for the given fallback. If either * of both doesn't provide a translation for the given language, the translation for the default * language is returned. If neither of both keys exist property will be returned. */ public static String safeGet(@Nonnull String property, @Nonnull String fallback) { return blubb.get(property, fallback, true).translate(getCurrentLang(), getFallbackLanguage()); } /** * Translates the given string if it starts with a $ sign. * * @param keyOrString the string to translate if it starts with a $ sign * @return the translated string or the original string if it doesn't start with a $ sign or if no matching * translation was found */ public static String smartGet(@Nonnull String keyOrString) { return smartGet(keyOrString, null); } /** * Translates the given string if it starts with a $ sign. * * @param keyOrString the string to translate if it starts with a $ sign * @param lang a two-letter language code for which the translation is requested * @return the translated string or the original string if it doesn't start with a $ sign or if no matching * translation was found */ @SuppressWarnings("squid:S2583") @Explain("Duplicate null check as predicate is not enforced by the compiler") public static String smartGet(@Nonnull String keyOrString, @Nullable String lang) { if (keyOrString == null) { return keyOrString; } if (keyOrString.length() > 2 && keyOrString.charAt(0) == '$' && keyOrString.charAt(1) != '{') { return NLS.getIfExists(keyOrString.substring(1), lang).orElseGet(() -> keyOrString.substring(1)); } return keyOrString; } /** * Creates a formatted using the pattern supplied by the translation value for the given property. * * @param property the property to used to retrieve a translated pattern * @return a Formatter initialized with the translated text of the given property */ public static Formatter fmtr(@Nonnull String property) { return Formatter.create(get(property), getCurrentLang()); } /** * Creates a formatted using the pattern supplied by the translation value for the given property. * smasmassm * * @param property the property to used to retrieve a translated pattern * @param lang a two-letter language code for which the translation is requested * @return a Formatter initialized with the translated text of the given property */ public static Formatter fmtr(@Nonnull String property, @Nullable String lang) { return Formatter.create(get(property, lang), getCurrentLang()); } /** * Marks a string as deliberately not translated. *

* Can be used to signal that a string needs no internationalization as it is only used on rare cases etc. * * @param s the text which will be used as output * @return the given value for s */ public static String nonNLS(String s) { return s; } /** * Provides access to commonly used keys. */ public enum CommonKeys { YES, NO, OK, CANCEL, NAME, EDIT, DELETE, SEARCH, SEARCHKEY, REFRESH, CLOSE, DESCRIPTION, SAVE, NEW, BACK, FILTER, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY, JANUARY, FEBRUARY, MARCH, APRIL, MAY, JUNE, JULY, AUGUST, SEPTEMBER, OCTOBER, NOVEMBER, DECEMBER; /** * Returns the fully qualified key to retrieve the translation * * @return the fully qualified key which can be supplied to NLS.get */ public String key() { return "NLS." + name().toLowerCase(); } /** * Returns the translation for this key in the current language. * * @return the translation for this key */ public String translated() { return get(key()); } } /** * Converts a given integer ({@code Calendar.Monday...Calendar.Sunday}) into textual their representation. * * @param day the weekday to be translated. Use constants {@link Calendar#MONDAY} etc. * @return the name of the given weekday in the current language * or {@code ""} if an invalid index was given */ public static String getDayOfWeek(int day) { switch (day) { case Calendar.MONDAY: return CommonKeys.MONDAY.translated(); case Calendar.TUESDAY: return CommonKeys.TUESDAY.translated(); case Calendar.WEDNESDAY: return CommonKeys.WEDNESDAY.translated(); case Calendar.THURSDAY: return CommonKeys.THURSDAY.translated(); case Calendar.FRIDAY: return CommonKeys.FRIDAY.translated(); case Calendar.SATURDAY: return CommonKeys.SATURDAY.translated(); case Calendar.SUNDAY: return CommonKeys.SUNDAY.translated(); default: return ""; } } /** * Returns a two letter abbreviation of the name of the given day, like {@code "Mo"}. * * @param day the weekday to be translated. Use constants {@link Calendar#MONDAY} etc. * @return returns the first two letters of the name * or {@code ""} if the given index was invalid. */ public static String getDayOfWeekShort(int day) { return Value.of(getDayOfWeek(day)).substring(0, 2); } /** * Returns the name of the given month in the current language * * @param month the month which name is requested (1..12) * @return the name of the given month translated in the current language * or "" if an invalid index was given */ public static String getMonthName(int month) { switch (month) { case 1: return CommonKeys.JANUARY.translated(); case 2: return CommonKeys.FEBRUARY.translated(); case 3: return CommonKeys.MARCH.translated(); case 4: return CommonKeys.APRIL.translated(); case 5: return CommonKeys.MAY.translated(); case 6: return CommonKeys.JUNE.translated(); case 7: return CommonKeys.JULY.translated(); case 8: return CommonKeys.AUGUST.translated(); case 9: return CommonKeys.SEPTEMBER.translated(); case 10: return CommonKeys.OCTOBER.translated(); case 11: return CommonKeys.NOVEMBER.translated(); case 12: return CommonKeys.DECEMBER.translated(); default: return ""; } } /** * Returns a three letter abbreviation of the name of the given month, like "Jan". * * @param month the month to be translated (January is 1, December is 12). * @return returns the first three letters of the name * or "" if the given index was invalid. */ public static String getMonthNameShort(int month) { return getMonthNameShort(month, ""); } /** * Returns a three letter abbreviation of the name of the given month, like "Jan". * If the name is short and has at most 4 characters, the name of the given month is returned instead. * The given symbol is only appended if the month was abbreviated so for example you get "Jan." * but with "May" the symbol String is not appended. * * @param month the month to be translated (January is 1, December is 12). * @param symbol the symbol to append in case of abbreviation * @return returns the first three letters of the name, the name of the month if short enough * or "" if the given index was invalid. */ public static String getMonthNameShort(int month, String symbol) { String result = getMonthName(month); if (result.length() > 4) { result = result.substring(0, 3) + symbol; } return result; } /** * Returns the date format for the given language. * * @param lang the language for which the format is requested * @return a format initialized with the pattern described by the given language */ public static DateTimeFormatter getDateFormat(String lang) { return dateFormatters.computeIfAbsent(lang, l -> DateTimeFormatter.ofPattern(get("NLS.patternDate", l))); } /** * Returns the short date format (two digit year like 24.10.14) for the given language. * * @param lang the language for which the format is requested * @return a format initialized with the pattern described by the given language */ public static DateTimeFormatter getShortDateFormat(String lang) { return shortDateFormatters.computeIfAbsent(lang, l -> DateTimeFormatter.ofPattern(get("NLS.patternShortDate", l))); } /** * Returns the full time format (with seconds) for the given language. *

* This should be used to format dates (times). Use {@link #getTimeParseFormat(String)} to parse strings * as it is more reluctant (or use {@link #parseUserString(Class, String)}). *

* The pattern in this case will conform to the PHP 5 patterns as these are used by some JavaScript * libraries like jQuery timepicker. (See http://php.net/manual/en/function.date.php). * * @param lang the language for which the format is requested * @return a format initialized with the pattern described by the given language */ public static DateTimeFormatter getTimeFormatWithSeconds(String lang) { return fullTimeFormatters.computeIfAbsent(lang, l -> DateTimeFormatter.ofPattern(get("NLS.patternFullTime", l))); } /** * Returns the time format (without seconds) for the given language. *

* This should be used to format dates (times). Use {@link #getTimeParseFormat(String)} to parse strings * as it is more reluctant (or use {@link #parseUserString(Class, String)}). *

* * @param lang the language for which the format is requested * @return a format initialized with the pattern described by the given language */ public static DateTimeFormatter getTimeFormat(String lang) { return timeFormatters.computeIfAbsent(lang, l -> DateTimeFormatter.ofPattern(get("NLS.patternTime", l))); } /** * Returns the time format which is intended to parse time value in the given language. *

* In contrast to {@link #getTimeFormat(String)} and {@link #getTimeFormatWithSeconds(String)} * this is used to parse dates and is more reluctant when it comes to formatting (parses '9:00' whereas * getTimeFormat(String) only accepts '09:00'). *

* * @param lang the language for which the format is requested * @return a format initialized with the pattern described by the given language */ public static DateTimeFormatter getTimeParseFormat(String lang) { return parseTimeFormatters.computeIfAbsent(lang, l -> DateTimeFormatter.ofPattern(get("NLS.patternParseTime", l))); } /** * Returns the date and time format (without seconds) for the given language. * * @param lang the language for which the format is requested * @return a format initialized with the pattern described by the given language */ public static DateTimeFormatter getDateTimeFormat(String lang) { return dateTimeFormatters.computeIfAbsent(lang, l -> DateTimeFormatter.ofPattern(get("NLS.patternDateTime", l))); } /** * Returns the format for the current language to format decimal numbers * * @return a format initialized with the pattern described by the current language */ public static java.text.NumberFormat getDecimalFormat() { return getDecimalFormat(getCurrentLang()); } /** * Returns the format for the given language to format decimal numbers * * @param lang the language for which the format is requested * @return a format initialized with the pattern described by the given language */ public static java.text.NumberFormat getDecimalFormat(String lang) { return new DecimalFormat(get("NLS.patternDecimal", lang), getDecimalFormatSymbols(lang)); } /** * Returns the decimal format symbols for the current language * * @return the decimal format symbols like thousands separator or decimal separator * as described by the current language */ public static DecimalFormatSymbols getDecimalFormatSymbols() { return getDecimalFormatSymbols(getCurrentLang()); } /** * Returns the decimal format symbols for the given language * * @param lang the two-letter code of the language for which the decimal format symbols should be returned * @return the decimal format symbols like thousands separator or decimal separator * as described by the given language */ public static DecimalFormatSymbols getDecimalFormatSymbols(String lang) { DecimalFormatSymbols sym = new DecimalFormatSymbols(); sym.setGroupingSeparator(get("NLS.groupingSeparator", lang).charAt(0)); sym.setDecimalSeparator(get("NLS.decimalSeparator", lang).charAt(0)); return sym; } /** * Creates a new decimal format symbols instance which is independent of the current language or locale and * constantly set to use '.' as decimal separator with no grouping separator. *

* This is commonly used to exchange numbers between machines. * * @return a decimal format symbols instance used for formatting numbers as "machine" strings */ public static DecimalFormatSymbols getMachineFormatSymbols() { DecimalFormatSymbols sym = new DecimalFormatSymbols(); sym.setDecimalSeparator('.'); return sym; } /** * Formats the given data in a language independent format. * * @param data the input data which should be converted to string * @return string representation of the given object, which can be parsed by * {@link #parseMachineString(Class, String)} independently of the language settings */ @SuppressWarnings({"squid:MethodCyclomaticComplexity", "squid:S3776"}) @Explain("The high complexity as acceptable as it is basically just a list of if statements") public static String toMachineString(Object data) { if (data == null) { return ""; } if (data instanceof String) { return ((String) data).trim(); } if (data instanceof Boolean) { return data.toString(); } if (data instanceof Temporal) { // Convert Instant to LocalDateTime to permit a "normal" time format if (data instanceof Instant) { data = LocalDateTime.ofInstant((Instant) data, ZoneId.systemDefault()); } Temporal temporal = (Temporal) data; if (ChronoUnit.HOURS.isSupportedBy(temporal)) { if (!ChronoField.DAY_OF_MONTH.isSupportedBy(temporal)) { return MACHINE_FORMAT_TIME_FORMAT.format(temporal); } else { return MACHINE_DATE_TIME_FORMAT.format(temporal); } } else { return MACHINE_DATE_FORMAT.format(temporal); } } if (data instanceof Integer) { return String.valueOf(data); } if (data instanceof Long) { return String.valueOf(data); } if (data instanceof Amount) { return ((Amount) data).toString(NumberFormat.MACHINE_TWO_DECIMAL_PLACES).asString(); } if (data instanceof BigDecimal) { return ((BigDecimal) data).toPlainString(); } if (data instanceof Double) { return String.valueOf(data); } if (data.getClass().isEnum()) { return ((Enum) data).name(); } if (data instanceof Float) { return String.valueOf(data); } if (data instanceof Throwable) { return writeThreadStrace((Throwable) data); } return String.valueOf(data); } private static String writeThreadStrace(Throwable data) { StringWriter writer = new StringWriter(); PrintWriter pw = new PrintWriter(writer); data.printStackTrace(pw); String result = writer.toString(); pw.close(); return result; } /** * Formats the given data according to the format rules of the current language * * @param object the object to be converted to a string * @return a string representation of the given object, formatted by the language settings of the current language */ public static String toUserString(Object object) { return toUserString(object, getCurrentLang()); } /** * Formats the given data according to the format rules of the given language * * @param data the object to be converted to a string * @param lang a two-letter language code for which the translation is requested * @return a string representation of the given object, formatted by the language settings of the current language */ @SuppressWarnings("squid:S3776") @Explain("The high complexity as acceptable as it is basically just a list of if statements") public static String toUserString(Object data, String lang) { if (data == null) { return ""; } if (data instanceof String) { return ((String) data).trim(); } if (data instanceof Boolean) { if (((Boolean) data).booleanValue()) { return NLS.get(CommonKeys.YES.key(), lang); } else { return NLS.get(CommonKeys.NO.key(), lang); } } if (data instanceof Temporal) { // Convert Instant to LocalDateTime to permit a "normal" time format if (data instanceof Instant) { data = LocalDateTime.ofInstant((Instant) data, ZoneId.systemDefault()); } Temporal temporal = (Temporal) data; if (ChronoUnit.HOURS.isSupportedBy(temporal)) { if (!ChronoField.DAY_OF_MONTH.isSupportedBy(temporal)) { return getTimeFormatWithSeconds(lang).format(temporal); } else { return getDateTimeFormat(lang).format(temporal); } } else { return getDateFormat(lang).format(temporal); } } if (data instanceof Integer) { return String.valueOf(data); } if (data instanceof Long) { return String.valueOf(data); } if (data instanceof BigDecimal) { return getDecimalFormat(lang).format(((BigDecimal) data).doubleValue()); } if (data instanceof Double) { return getDecimalFormat(lang).format(data); } if (data instanceof Float) { return getDecimalFormat(lang).format(data); } if (data instanceof Throwable) { return writeThreadStrace((Throwable) data); } return String.valueOf(data); } /** * Converts dates to a "human" (e.g. "today", "yesterday") format. *

* The following texts are supported: *

    *
  • If the given date contains a time and is less than 30 min ago: "some minutes ago"
  • *
  • If the given date contains a time and is less than 60 min ago: "N minutes ago"
  • *
  • If the given date contains a time and is less than 2 hours ago: "one hour ago"
  • *
  • If the given date contains a time and is less than 6 hours ago: "N hours ago"
  • *
  • If the given date is today: "today"
  • *
  • If the given date is tomorrow: "tomorrow"
  • *
  • If the given date is yesterday: "yesterday"
  • *
  • For everything else we use the date format (note that in this case the time is omitted
  • *
* * @param date the date to be formatted * @return a date string which a human would use in common sentences */ public static String toSpokenDate(Temporal date) { if (date == null) { return ""; } if (ChronoUnit.HOURS.isSupportedBy(date)) { return formatSpokenDateWithTime(date); } else { return formatSpokenDate(date); } } private static String formatSpokenDate(Temporal date) { // Check if we have a date which is not "today".... LocalDate givenDate = LocalDate.from(date); LocalDate tomorrow = LocalDate.now().plusDays(1); if (tomorrow.equals(givenDate)) { // Handle tomorrow return NLS.get("NLS.tomorrow"); } LocalDate yesterday = LocalDate.now().minusDays(1); if (yesterday.equals(givenDate)) { // Handle yesterday return NLS.get("NLS.yesterday"); } if (tomorrow.isBefore(givenDate) || yesterday.isAfter(givenDate)) { // Handle dates in the future or the past return getDateFormat(getCurrentLang()).format(givenDate); } else { return NLS.get("NLS.today"); } } private static String formatSpokenDateWithTime(Temporal date) { // We have a time, perform some nice formatting... LocalDateTime givenDateTime = LocalDateTime.from(date); if (givenDateTime.isAfter(LocalDateTime.now())) { return formatSpokenFutureDateWithTime(date, givenDateTime); } if (givenDateTime.isAfter(LocalDateTime.now().minusHours(12))) { return formatSpokenRecentDateWithTime(givenDateTime); } if (!ChronoField.DAY_OF_MONTH.isSupportedBy(date)) { // We don't have a date and the time difference is quite big -> simply format the time... return getTimeFormat(getCurrentLang()).format(date); } return formatSpokenDate(date); } private static String formatSpokenFutureDateWithTime(Temporal date, LocalDateTime givenDateTime) { if (givenDateTime.isBefore(LocalDateTime.now().plusHours(1))) { return NLS.get("NLS.nextHour"); } if (givenDateTime.isBefore(LocalDateTime.now().plusHours(4))) { return NLS.fmtr("NLS.inNHours") .set("hours", Duration.between(LocalDateTime.now(), givenDateTime).toHours()) .format(); } if (ChronoField.DAY_OF_MONTH.isSupportedBy(date) && !LocalDate.now().equals(LocalDate.from(date))) { return formatSpokenDate(date); } return getTimeFormat(getCurrentLang()).format(date); } private static String formatSpokenRecentDateWithTime(LocalDateTime givenDateTime) { if (givenDateTime.isAfter(LocalDateTime.now().minusMinutes(30))) { return NLS.get("NLS.someMinutesAgo"); } if (givenDateTime.isAfter(LocalDateTime.now().minusMinutes(59))) { return NLS.fmtr("NLS.nMinutesAgo") .set("minutes", Duration.between(givenDateTime, LocalDateTime.now()).toMinutes()) .format(); } if (givenDateTime.isAfter(LocalDateTime.now().minusHours(2))) { return NLS.get("NLS.oneHourAgo"); } return NLS.fmtr("NLS.nHoursAgo") .set("hours", Duration.between(givenDateTime, LocalDateTime.now()).toHours()) .format(); } /** * Parses the given string by expecting a machine independent format. *

* This can parse all strings generated by toMachineString * * @param clazz the expected class of the value to be parsed * @param value the string to be parsed * @param the target type be be parsed * @return an instance of clazz representing the parsed string or null if value was empty. * @throws IllegalArgumentException if the given input was not well formed or if instances of clazz * cannot be created. The thrown exception has a translated error message which * can be directly presented to the user. */ @SuppressWarnings("unchecked") public static V parseMachineString(Class clazz, String value) { if (Strings.isEmpty(value)) { return null; } if (String.class.equals(clazz)) { return (V) value; } return parseBasicTypesFromMachineString(clazz, value); } @SuppressWarnings({"unchecked", "squid:S3776"}) @Explain("The high complexity as acceptable as it is basically just a list of if statements") private static V parseBasicTypesFromMachineString(Class clazz, String value) { if (Integer.class.equals(clazz) || int.class.equals(clazz)) { try { return (V) Integer.valueOf(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidNumber").set("value", value).format(), e); } } if (Long.class.equals(clazz) || long.class.equals(clazz)) { try { return (V) Long.valueOf(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidNumber").set("value", value).format(), e); } } if (Float.class.equals(clazz) || float.class.equals(clazz)) { try { Double result = Double.valueOf(value); return (result == null) ? null : (V) Float.valueOf(result.floatValue()); } catch (NumberFormatException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidDecimalNumber").set("value", value).format(), e); } } if (Double.class.equals(clazz) || double.class.equals(clazz)) { try { return (V) Double.valueOf(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidDecimalNumber").set("value", value).format(), e); } } if (BigDecimal.class.equals(clazz)) { try { return (V) new BigDecimal(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidDecimalNumber").set("value", value).format(), e); } } if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) { return (V) Boolean.valueOf(Boolean.parseBoolean(value)); } return parseDatesFromMachineString(clazz, value); } @SuppressWarnings("unchecked") private static V parseDatesFromMachineString(Class clazz, String value) { if (LocalDate.class.equals(clazz)) { try { return (V) LocalDate.from(MACHINE_DATE_FORMAT.parse(value)); } catch (DateTimeParseException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidDate").set("value", value) .set("format", "yyyy-MM-dd") .format(), e); } } if (LocalDateTime.class.equals(clazz)) { try { return (V) LocalDateTime.from(MACHINE_DATE_TIME_FORMAT.parse(value)); } catch (DateTimeParseException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidDate").set("value", value) .set("format", "yyyy-MM-dd HH:mm:ss") .format(), e); } } if (ZonedDateTime.class.equals(clazz)) { try { return (V) ZonedDateTime.from(MACHINE_DATE_TIME_FORMAT.parse(value)); } catch (DateTimeParseException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidDate").set("value", value) .set("format", "yyyy-MM-dd HH:mm:ss") .format(), e); } } if (LocalTime.class.equals(clazz)) { try { return (V) LocalTime.from(MACHINE_PARSE_TIME_FORMAT.parse(value)); } catch (DateTimeParseException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidDate").set("value", value) .set("format", "H:mm[:ss]") .format(), e); } } throw new IllegalArgumentException(fmtr("NLS.parseError").set("type", clazz).format()); } /** * Parses the given string by expecting a format as defined by the given language. * * @param clazz the expected class of the value to be parsed * @param value the string to be parsed * @param lang the two-letter code of the language which format should be used * @param the target type be be parsed * @return an instance of clazz representing the parsed string or null if value was empty. * @throws IllegalArgumentException if the given input was not well formed or if instances of clazz * cannot be created. The thrown exception has a translated error message which * can be directly presented to the user. */ @SuppressWarnings("unchecked") public static V parseUserString(Class clazz, String value, String lang) { if (Strings.isEmpty(value)) { return null; } if (String.class.equals(clazz)) { return (V) value; } return parseBasicTypesFromUserString(clazz, value, lang); } @SuppressWarnings({"unchecked", "squid:S3776"}) @Explain("The high complexity as acceptable as it is basically just a list of if statements") private static V parseBasicTypesFromUserString(Class clazz, String value, String lang) { if (Integer.class.equals(clazz) || int.class.equals(clazz)) { try { return (V) Integer.valueOf(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidNumber").set("value", value).format(), e); } } if (Long.class.equals(clazz) || long.class.equals(clazz)) { try { return (V) Long.valueOf(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidNumber").set("value", value).format(), e); } } if (Float.class.equals(clazz) || float.class.equals(clazz)) { return (V) Float.valueOf((float) parseDecimalNumberFromUser(value, lang).doubleValue()); } if (Double.class.equals(clazz) || double.class.equals(clazz)) { return (V) parseDecimalNumberFromUser(value, lang); } if (Amount.class.equals(clazz)) { return (V) Amount.of(parseDecimalNumberFromUser(value, lang)); } if (Boolean.class.equals(clazz) || boolean.class.equals(clazz)) { if (NLS.get(CommonKeys.YES.key(), lang).equalsIgnoreCase(value)) { return (V) Boolean.TRUE; } if (NLS.get(CommonKeys.NO.key(), lang).equalsIgnoreCase(value)) { return (V) Boolean.FALSE; } return (V) Boolean.valueOf(Boolean.parseBoolean(value)); } return parseDatesFromUserString(clazz, value, lang); } private static Double parseDecimalNumberFromUser(String value, String lang) { try { Double result = tryParseMachineFormat(value); if (result != null) { return result; } return getDecimalFormat(lang).parse(value).doubleValue(); } catch (ParseException e) { Exceptions.ignore(e); return Double.valueOf(value); } catch (NumberFormatException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidDecimalNumber").set("value", value).format(), e); } } /** * If there is exactly one "." in the pattern and no "," and we have less then 3 digits behind the "." we treat this * as english decimal format and not as german grouping separator. * * @param value the parsed value or null if the format doesn't match * @return true if the format being used is a english / technical one and not a german one where "." is the * thousand separator */ private static Double tryParseMachineFormat(String value) { if (!".".equals(NLS.get("NLS.groupingSeparator"))) { return null; } if (!value.contains(".") || value.contains(",")) { return null; } if (value.indexOf('.') == value.lastIndexOf('.') && value.indexOf('.') > value.length() - 4) { try { return Double.valueOf(value); } catch (Exception e) { Exceptions.ignore(e); } } return null; } @SuppressWarnings("unchecked") private static V parseDatesFromUserString(Class clazz, String value, String lang) { if (LocalDate.class.equals(clazz)) { try { AdvancedDateParser parser = new AdvancedDateParser(lang); return (V) LocalDate.from(parser.parse(value).getTemporal()); } catch (ParseException e) { throw new IllegalArgumentException(e.getMessage(), e); } } if (LocalDateTime.class.equals(clazz)) { try { AdvancedDateParser parser = new AdvancedDateParser(lang); return (V) LocalDateTime.from(parser.parse(value).getTemporal()); } catch (ParseException e) { throw new IllegalArgumentException(e.getMessage(), e); } } if (ZonedDateTime.class.equals(clazz)) { try { AdvancedDateParser parser = new AdvancedDateParser(lang); return (V) ZonedDateTime.from(parser.parse(value).getTemporal()); } catch (ParseException e) { throw new IllegalArgumentException(e.getMessage(), e); } } if (LocalTime.class.equals(clazz)) { try { return (V) LocalTime.from(NLS.getTimeParseFormat(lang).parse(value.toUpperCase())); } catch (DateTimeParseException e) { throw new IllegalArgumentException(fmtr("NLS.errInvalidTime").set("value", value).format(), e); } } if (AdvancedDateParser.DateSelection.class.equals(clazz)) { try { AdvancedDateParser parser = new AdvancedDateParser(lang); return (V) parser.parse(value); } catch (ParseException e) { throw new IllegalArgumentException(e.getMessage(), e); } } throw new IllegalArgumentException(fmtr("NLS.parseError").set("type", clazz).format()); } /** * Parses the given string by expecting a format as defined by the current language. * * @param clazz the expected class of the value to be parsed * @param string the string to be parsed * @param the target type be be parsed * @return an instance of clazz representing the parsed string or null if value was empty. * @throws IllegalArgumentException if the given input was not well formed or if instances of clazz * cannot be created. The thrown exception has a translated error message which * can be directly presented to the user. */ public static V parseUserString(Class clazz, String string) { return parseUserString(clazz, string, getCurrentLang()); } /** * Converts a given time range in milliseconds to a human readable format using the current language * * @param duration the duration in milliseconds * @param includeSeconds determines whether to include seconds or to ignore everything below minutes * @param includeMillis determines whether to include milli seconds or to ignore everything below seconds * @return a string representation of the given duration in days, hours, minutes and, * if enabled, seconds and milliseconds */ public static String convertDuration(long duration, boolean includeSeconds, boolean includeMillis) { StringBuilder result = new StringBuilder(); if (duration > DAY) { appendDurationValue(result, "NLS.day", "NLS.days", duration / DAY); duration = duration % DAY; } if (duration > HOUR) { appendDurationValue(result, "NLS.hour", "NLS.hours", duration / HOUR); duration = duration % HOUR; } if (duration > MINUTE || (!includeSeconds && duration > 0)) { appendDurationValue(result, "NLS.minute", "NLS.minutes", duration / MINUTE); duration = duration % MINUTE; } if (includeSeconds) { if (duration > SECOND || (!includeMillis && duration > 0)) { appendDurationValue(result, "NLS.second", "NLS.seconds", duration / SECOND); duration = duration % SECOND; } if (includeMillis && duration > 0) { appendDurationValue(result, "NLS.millisecond", "NLS.milliseconds", duration); } } return result.toString(); } private static void appendDurationValue(StringBuilder result, String oneKey, String manyKey, long value) { if (result.length() > 0) { result.append(", "); } if (value == 1) { result.append(Strings.apply(NLS.get(oneKey), 1)); } else { result.append(Strings.apply(NLS.get(manyKey), value)); } } /** * Converts the given duration in milliseconds including seconds and milliseconds *

* This is a boilerplate method for {@link #convertDuration(long, boolean, boolean)} with * includeSeconds and includeMillis set to true. * * @param duration the duration in milliseconds * @return a string representation of the given duration in days, hours, minutes, seconds and milliseconds */ public static String convertDuration(long duration) { return convertDuration(duration, true, true); } /** * Outputs integer numbers without decimals, but fractional numbers with two digits. *

* Discards fractional parts which absolute value is less or equal to {@code 0.00001}. * * @param number the number to be rounded * @return a string representation using the current languages decimal format. * Rounds fractional parts less or equal to 0.00001 */ public static String smartRound(double number) { if (Math.abs(Math.floor(number) - number) > 0.000001D) { return NLS.toUserString(number); } else { return String.valueOf(Math.round(number)); } } /** * Converts a file or byte size. *

* Supports sizes up to petabyte. Uses conventional SI-prefixed abbreviations like kB, MB. * * @param size the size to format in bytes * @return an english representation (using dot as decimal separator) along with one of the known abbreviations: * Bytes, KB, MB, GB, TB, PB. */ public static String formatSize(long size) { int index = 0; double sizeAsFloat = size; while (sizeAsFloat > 1000 && index < UNITS.length - 1) { sizeAsFloat = sizeAsFloat / 1000; index++; } return Amount.of(sizeAsFloat).toSmartRoundedString(NumberFormat.MACHINE_TWO_DECIMAL_PLACES) + " " + UNITS[index]; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy