Index: /trunk/.classpath
===================================================================
--- /trunk/.classpath	(revision 12930)
+++ /trunk/.classpath	(revision 12931)
@@ -15,5 +15,5 @@
 	<classpathentry kind="lib" path="test/lib/fest/MRJToolkitStubs-1.0.jar"/>
 	<classpathentry kind="lib" path="test/lib/jfcunit.jar"/>
-	<classpathentry kind="lib" path="test/lib/equalsverifier-2.3.3.jar"/>
+	<classpathentry exported="true" kind="lib" path="test/lib/equalsverifier-2.3.3.jar"/>
 	<classpathentry kind="lib" path="test/lib/reflections/reflections-0.9.10.jar"/>
 	<classpathentry kind="lib" path="test/lib/reflections/guava-21.0.jar"/>
Index: /trunk/build.xml
===================================================================
--- /trunk/build.xml	(revision 12930)
+++ /trunk/build.xml	(revision 12931)
@@ -64,4 +64,6 @@
         <property name="revision.dir" value="bin"/>
         <antcall target="create-revision"/>
+        <mkdir dir="bin/META-INF/services"/>
+        <echo encoding="UTF-8" file="bin/META-INF/services/java.text.spi.DecimalFormatSymbolsProvider">org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider</echo>
     </target>
     <!--
@@ -151,4 +153,5 @@
                 <attribute name="Add-Opens" value="java.base/java.lang java.base/jdk.internal.loader java.desktop/javax.imageio.spi java.desktop/javax.swing.text.html java.prefs/java.util.prefs" />
             </manifest>
+            <service type="java.text.spi.DecimalFormatSymbolsProvider" provider="org.openstreetmap.josm.tools.JosmDecimalFormatSymbolsProvider" />
             <zipfileset dir="images" prefix="images"/>
             <zipfileset dir="data" prefix="data"/>
Index: /trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java	(revision 12930)
+++ /trunk/src/org/openstreetmap/josm/gui/bbox/SlippyMapBBoxChooser.java	(revision 12931)
@@ -225,5 +225,5 @@
     public void paintComponent(Graphics g) {
         super.paintComponent(g);
-        Graphics2D g2d = (Graphics2D)g;
+        Graphics2D g2d = (Graphics2D) g;
 
         // draw shaded area for non-downloaded region of current "edit layer", but only if there *is* a current "edit layer",
Index: /trunk/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java	(revision 12930)
+++ /trunk/src/org/openstreetmap/josm/gui/preferences/server/AuthenticationPreferencesPanel.java	(revision 12931)
@@ -18,5 +18,4 @@
 import javax.swing.JSeparator;
 
-import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
 import org.openstreetmap.josm.gui.help.HelpUtil;
Index: /trunk/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java	(revision 12930)
+++ /trunk/src/org/openstreetmap/josm/gui/preferences/server/OAuthAuthenticationPreferencesPanel.java	(revision 12931)
@@ -23,5 +23,4 @@
 import javax.swing.JPanel;
 
-import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
 import org.openstreetmap.josm.data.oauth.OAuthParameters;
Index: /trunk/src/org/openstreetmap/josm/io/OsmConnection.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/OsmConnection.java	(revision 12930)
+++ /trunk/src/org/openstreetmap/josm/io/OsmConnection.java	(revision 12931)
@@ -12,5 +12,4 @@
 import java.util.Objects;
 
-import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.data.oauth.OAuthAccessTokenHolder;
 import org.openstreetmap.josm.data.oauth.OAuthParameters;
Index: /trunk/src/org/openstreetmap/josm/tools/I18n.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/I18n.java	(revision 12930)
+++ /trunk/src/org/openstreetmap/josm/tools/I18n.java	(revision 12931)
@@ -90,225 +90,5 @@
     private static volatile Map<String, String[]> pstrings;
     private static Map<String, PluralMode> languages = new HashMap<>();
-
-    /**
-     * Translates some text for the current locale.
-     * These strings are collected by a script that runs on the source code files.
-     * After translation, the localizations are distributed with the main program.
-     * <br>
-     * For example, <code>tr("JOSM''s default value is ''{0}''.", val)</code>.
-     * <br>
-     * Use {@link #trn} for distinguishing singular from plural text, i.e.,
-     * do not use {@code tr(size == 1 ? "singular" : "plural")} nor
-     * {@code size == 1 ? tr("singular") : tr("plural")}
-     *
-     * @param text the text to translate.
-     * Must be a string literal. (No constants or local vars.)
-     * Can be broken over multiple lines.
-     * An apostrophe ' must be quoted by another apostrophe.
-     * @param objects the parameters for the string.
-     * Mark occurrences in {@code text} with <code>{0}</code>, <code>{1}</code>, ...
-     * @return the translated string.
-     * @see #trn
-     * @see #trc
-     * @see #trnc
-     */
-    public static String tr(String text, Object... objects) {
-        if (text == null) return null;
-        return MessageFormat.format(gettext(text, null), objects);
-    }
-
-    /**
-     * Translates some text in a context for the current locale.
-     * There can be different translations for the same text within different contexts.
-     *
-     * @param context string that helps translators to find an appropriate
-     * translation for {@code text}.
-     * @param text the text to translate.
-     * @return the translated string.
-     * @see #tr
-     * @see #trn
-     * @see #trnc
-     */
-    public static String trc(String context, String text) {
-        if (context == null)
-            return tr(text);
-        if (text == null)
-            return null;
-        return MessageFormat.format(gettext(text, context), (Object) null);
-    }
-
-    public static String trcLazy(String context, String text) {
-        if (context == null)
-            return tr(text);
-        if (text == null)
-            return null;
-        return MessageFormat.format(gettextLazy(text, context), (Object) null);
-    }
-
-    /**
-     * Marks a string for translation (such that a script can harvest
-     * the translatable strings from the source files).
-     *
-     * For example, <code>
-     * String[] options = new String[] {marktr("up"), marktr("down")};
-     * lbl.setText(tr(options[0]));</code>
-     * @param text the string to be marked for translation.
-     * @return {@code text} unmodified.
-     */
-    public static String marktr(String text) {
-        return text;
-    }
-
-    public static String marktrc(String context, String text) {
-        return text;
-    }
-
-    /**
-     * Translates some text for the current locale and distinguishes between
-     * {@code singularText} and {@code pluralText} depending on {@code n}.
-     * <br>
-     * For instance, {@code trn("There was an error!", "There were errors!", i)} or
-     * <code>trn("Found {0} error in {1}!", "Found {0} errors in {1}!", i, Integer.toString(i), url)</code>.
-     *
-     * @param singularText the singular text to translate.
-     * Must be a string literal. (No constants or local vars.)
-     * Can be broken over multiple lines.
-     * An apostrophe ' must be quoted by another apostrophe.
-     * @param pluralText the plural text to translate.
-     * Must be a string literal. (No constants or local vars.)
-     * Can be broken over multiple lines.
-     * An apostrophe ' must be quoted by another apostrophe.
-     * @param n a number to determine whether {@code singularText} or {@code pluralText} is used.
-     * @param objects the parameters for the string.
-     * Mark occurrences in {@code singularText} and {@code pluralText} with <code>{0}</code>, <code>{1}</code>, ...
-     * @return the translated string.
-     * @see #tr
-     * @see #trc
-     * @see #trnc
-     */
-    public static String trn(String singularText, String pluralText, long n, Object... objects) {
-        return MessageFormat.format(gettextn(singularText, pluralText, null, n), objects);
-    }
-
-    /**
-     * Translates some text in a context for the current locale and distinguishes between
-     * {@code singularText} and {@code pluralText} depending on {@code n}.
-     * There can be different translations for the same text within different contexts.
-     *
-     * @param context string that helps translators to find an appropriate
-     * translation for {@code text}.
-     * @param singularText the singular text to translate.
-     * Must be a string literal. (No constants or local vars.)
-     * Can be broken over multiple lines.
-     * An apostrophe ' must be quoted by another apostrophe.
-     * @param pluralText the plural text to translate.
-     * Must be a string literal. (No constants or local vars.)
-     * Can be broken over multiple lines.
-     * An apostrophe ' must be quoted by another apostrophe.
-     * @param n a number to determine whether {@code singularText} or {@code pluralText} is used.
-     * @param objects the parameters for the string.
-     * Mark occurrences in {@code singularText} and {@code pluralText} with <code>{0}</code>, <code>{1}</code>, ...
-     * @return the translated string.
-     * @see #tr
-     * @see #trc
-     * @see #trn
-     */
-    public static String trnc(String context, String singularText, String pluralText, long n, Object... objects) {
-        return MessageFormat.format(gettextn(singularText, pluralText, context, n), objects);
-    }
-
-    private static String gettext(String text, String ctx, boolean lazy) {
-        int i;
-        if (ctx == null && text.startsWith("_:") && (i = text.indexOf('\n')) >= 0) {
-            ctx = text.substring(2, i-1);
-            text = text.substring(i+1);
-        }
-        if (strings != null) {
-            String trans = strings.get(ctx == null ? text : "_:"+ctx+'\n'+text);
-            if (trans != null)
-                return trans;
-        }
-        if (pstrings != null) {
-            i = pluralEval(1);
-            String[] trans = pstrings.get(ctx == null ? text : "_:"+ctx+'\n'+text);
-            if (trans != null && trans.length > i)
-                return trans[i];
-        }
-        return lazy ? gettext(text, null) : text;
-    }
-
-    private static String gettext(String text, String ctx) {
-        return gettext(text, ctx, false);
-    }
-
-    /* try without context, when context try fails */
-    private static String gettextLazy(String text, String ctx) {
-        return gettext(text, ctx, true);
-    }
-
-    private static String gettextn(String text, String plural, String ctx, long num) {
-        int i;
-        if (ctx == null && text.startsWith("_:") && (i = text.indexOf('\n')) >= 0) {
-            ctx = text.substring(2, i-1);
-            text = text.substring(i+1);
-        }
-        if (pstrings != null) {
-            i = pluralEval(num);
-            String[] trans = pstrings.get(ctx == null ? text : "_:"+ctx+'\n'+text);
-            if (trans != null && trans.length > i)
-                return trans[i];
-        }
-
-        return num == 1 ? text : plural;
-    }
-
-    public static String escape(String msg) {
-        if (msg == null) return null;
-        return msg.replace("\'", "\'\'").replace("{", "\'{\'").replace("}", "\'}\'");
-    }
-
-    private static URL getTranslationFile(String lang) {
-        return I18n.class.getResource("/data/"+lang.replace('@', '-')+".lang");
-    }
-
-    /**
-     * Get a list of all available JOSM Translations.
-     * @return an array of locale objects.
-     */
-    public static Locale[] getAvailableTranslations() {
-        Collection<Locale> v = new ArrayList<>(languages.size());
-        if (getTranslationFile("en") != null) {
-            for (String loc : languages.keySet()) {
-                if (getTranslationFile(loc) != null) {
-                    v.add(LanguageInfo.getLocale(loc));
-                }
-            }
-        }
-        v.add(Locale.ENGLISH);
-        Locale[] l = new Locale[v.size()];
-        l = v.toArray(l);
-        Arrays.sort(l, Comparator.comparing(Locale::toString));
-        return l;
-    }
-
-    /**
-     * Determines if a language exists for the given code.
-     * @param code The language code
-     * @return {@code true} if a language exists, {@code false} otherwise
-     */
-    public static boolean hasCode(String code) {
-        return languages.containsKey(code);
-    }
-
-    /**
-     * I18n initialization.
-     */
-    public static void init() {
-        // Enable CLDR locale provider on Java 8 to get additional languages, such as Khmer.
-        // http://docs.oracle.com/javase/8/docs/technotes/guides/intl/enhancements.8.html#cldr
-        // FIXME: This can be removed after we switch to a minimal version of Java that enables CLDR by default
-        // or includes all languages we need in the JRE. See http://openjdk.java.net/jeps/252 for Java 9
-        System.setProperty("java.locale.providers", "JRE,CLDR"); // Don't call Utils.updateSystemProperty to avoid spurious log at startup
-
+    static {
         //languages.put("ar", PluralMode.MODE_AR);
         languages.put("ast", PluralMode.MODE_NOTONE);
@@ -353,4 +133,230 @@
         languages.put("zh_CN", PluralMode.MODE_NONE);
         languages.put("zh_TW", PluralMode.MODE_NONE);
+    }
+
+    /**
+     * Translates some text for the current locale.
+     * These strings are collected by a script that runs on the source code files.
+     * After translation, the localizations are distributed with the main program.
+     * <br>
+     * For example, <code>tr("JOSM''s default value is ''{0}''.", val)</code>.
+     * <br>
+     * Use {@link #trn} for distinguishing singular from plural text, i.e.,
+     * do not use {@code tr(size == 1 ? "singular" : "plural")} nor
+     * {@code size == 1 ? tr("singular") : tr("plural")}
+     *
+     * @param text the text to translate.
+     * Must be a string literal. (No constants or local vars.)
+     * Can be broken over multiple lines.
+     * An apostrophe ' must be quoted by another apostrophe.
+     * @param objects the parameters for the string.
+     * Mark occurrences in {@code text} with <code>{0}</code>, <code>{1}</code>, ...
+     * @return the translated string.
+     * @see #trn
+     * @see #trc
+     * @see #trnc
+     */
+    public static String tr(String text, Object... objects) {
+        if (text == null) return null;
+        return MessageFormat.format(gettext(text, null), objects);
+    }
+
+    /**
+     * Translates some text in a context for the current locale.
+     * There can be different translations for the same text within different contexts.
+     *
+     * @param context string that helps translators to find an appropriate
+     * translation for {@code text}.
+     * @param text the text to translate.
+     * @return the translated string.
+     * @see #tr
+     * @see #trn
+     * @see #trnc
+     */
+    public static String trc(String context, String text) {
+        if (context == null)
+            return tr(text);
+        if (text == null)
+            return null;
+        return MessageFormat.format(gettext(text, context), (Object) null);
+    }
+
+    public static String trcLazy(String context, String text) {
+        if (context == null)
+            return tr(text);
+        if (text == null)
+            return null;
+        return MessageFormat.format(gettextLazy(text, context), (Object) null);
+    }
+
+    /**
+     * Marks a string for translation (such that a script can harvest
+     * the translatable strings from the source files).
+     *
+     * For example, <code>
+     * String[] options = new String[] {marktr("up"), marktr("down")};
+     * lbl.setText(tr(options[0]));</code>
+     * @param text the string to be marked for translation.
+     * @return {@code text} unmodified.
+     */
+    public static String marktr(String text) {
+        return text;
+    }
+
+    public static String marktrc(String context, String text) {
+        return text;
+    }
+
+    /**
+     * Translates some text for the current locale and distinguishes between
+     * {@code singularText} and {@code pluralText} depending on {@code n}.
+     * <br>
+     * For instance, {@code trn("There was an error!", "There were errors!", i)} or
+     * <code>trn("Found {0} error in {1}!", "Found {0} errors in {1}!", i, Integer.toString(i), url)</code>.
+     *
+     * @param singularText the singular text to translate.
+     * Must be a string literal. (No constants or local vars.)
+     * Can be broken over multiple lines.
+     * An apostrophe ' must be quoted by another apostrophe.
+     * @param pluralText the plural text to translate.
+     * Must be a string literal. (No constants or local vars.)
+     * Can be broken over multiple lines.
+     * An apostrophe ' must be quoted by another apostrophe.
+     * @param n a number to determine whether {@code singularText} or {@code pluralText} is used.
+     * @param objects the parameters for the string.
+     * Mark occurrences in {@code singularText} and {@code pluralText} with <code>{0}</code>, <code>{1}</code>, ...
+     * @return the translated string.
+     * @see #tr
+     * @see #trc
+     * @see #trnc
+     */
+    public static String trn(String singularText, String pluralText, long n, Object... objects) {
+        return MessageFormat.format(gettextn(singularText, pluralText, null, n), objects);
+    }
+
+    /**
+     * Translates some text in a context for the current locale and distinguishes between
+     * {@code singularText} and {@code pluralText} depending on {@code n}.
+     * There can be different translations for the same text within different contexts.
+     *
+     * @param context string that helps translators to find an appropriate
+     * translation for {@code text}.
+     * @param singularText the singular text to translate.
+     * Must be a string literal. (No constants or local vars.)
+     * Can be broken over multiple lines.
+     * An apostrophe ' must be quoted by another apostrophe.
+     * @param pluralText the plural text to translate.
+     * Must be a string literal. (No constants or local vars.)
+     * Can be broken over multiple lines.
+     * An apostrophe ' must be quoted by another apostrophe.
+     * @param n a number to determine whether {@code singularText} or {@code pluralText} is used.
+     * @param objects the parameters for the string.
+     * Mark occurrences in {@code singularText} and {@code pluralText} with <code>{0}</code>, <code>{1}</code>, ...
+     * @return the translated string.
+     * @see #tr
+     * @see #trc
+     * @see #trn
+     */
+    public static String trnc(String context, String singularText, String pluralText, long n, Object... objects) {
+        return MessageFormat.format(gettextn(singularText, pluralText, context, n), objects);
+    }
+
+    private static String gettext(String text, String ctx, boolean lazy) {
+        int i;
+        if (ctx == null && text.startsWith("_:") && (i = text.indexOf('\n')) >= 0) {
+            ctx = text.substring(2, i-1);
+            text = text.substring(i+1);
+        }
+        if (strings != null) {
+            String trans = strings.get(ctx == null ? text : "_:"+ctx+'\n'+text);
+            if (trans != null)
+                return trans;
+        }
+        if (pstrings != null) {
+            i = pluralEval(1);
+            String[] trans = pstrings.get(ctx == null ? text : "_:"+ctx+'\n'+text);
+            if (trans != null && trans.length > i)
+                return trans[i];
+        }
+        return lazy ? gettext(text, null) : text;
+    }
+
+    private static String gettext(String text, String ctx) {
+        return gettext(text, ctx, false);
+    }
+
+    /* try without context, when context try fails */
+    private static String gettextLazy(String text, String ctx) {
+        return gettext(text, ctx, true);
+    }
+
+    private static String gettextn(String text, String plural, String ctx, long num) {
+        int i;
+        if (ctx == null && text.startsWith("_:") && (i = text.indexOf('\n')) >= 0) {
+            ctx = text.substring(2, i-1);
+            text = text.substring(i+1);
+        }
+        if (pstrings != null) {
+            i = pluralEval(num);
+            String[] trans = pstrings.get(ctx == null ? text : "_:"+ctx+'\n'+text);
+            if (trans != null && trans.length > i)
+                return trans[i];
+        }
+
+        return num == 1 ? text : plural;
+    }
+
+    public static String escape(String msg) {
+        if (msg == null) return null;
+        return msg.replace("\'", "\'\'").replace("{", "\'{\'").replace("}", "\'}\'");
+    }
+
+    private static URL getTranslationFile(String lang) {
+        return I18n.class.getResource("/data/"+lang.replace('@', '-')+".lang");
+    }
+
+    /**
+     * Get a list of all available JOSM Translations.
+     * @return an array of locale objects.
+     */
+    public static Locale[] getAvailableTranslations() {
+        Collection<Locale> v = new ArrayList<>(languages.size());
+        if (getTranslationFile("en") != null) {
+            for (String loc : languages.keySet()) {
+                if (getTranslationFile(loc) != null) {
+                    v.add(LanguageInfo.getLocale(loc));
+                }
+            }
+        }
+        v.add(Locale.ENGLISH);
+        Locale[] l = new Locale[v.size()];
+        l = v.toArray(l);
+        Arrays.sort(l, Comparator.comparing(Locale::toString));
+        return l;
+    }
+
+    /**
+     * Determines if a language exists for the given code.
+     * @param code The language code
+     * @return {@code true} if a language exists, {@code false} otherwise
+     */
+    public static boolean hasCode(String code) {
+        return languages.containsKey(code);
+    }
+
+    static void setupJavaLocaleProviders() {
+        // Look up SPI providers first (for JosmDecimalFormatSymbolsProvider).
+        // Enable CLDR locale provider on Java 8 to get additional languages, such as Khmer.
+        // http://docs.oracle.com/javase/8/docs/technotes/guides/intl/enhancements.8.html#cldr
+        // FIXME: This must be updated after we switch to Java 9.
+        // See https://docs.oracle.com/javase/9/docs/api/java/util/spi/LocaleServiceProvider.html
+        System.setProperty("java.locale.providers", "SPI,JRE,CLDR"); // Don't call Utils.updateSystemProperty to avoid spurious log at startup
+    }
+
+    /**
+     * I18n initialization.
+     */
+    public static void init() {
+        setupJavaLocaleProviders();
 
         /* try initial language settings, may be changed later again */
Index: /trunk/src/org/openstreetmap/josm/tools/JosmDecimalFormatSymbolsProvider.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/JosmDecimalFormatSymbolsProvider.java	(revision 12931)
+++ /trunk/src/org/openstreetmap/josm/tools/JosmDecimalFormatSymbolsProvider.java	(revision 12931)
@@ -0,0 +1,30 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools;
+
+import java.text.DecimalFormatSymbols;
+import java.text.spi.DecimalFormatSymbolsProvider;
+import java.util.Locale;
+
+/**
+ * JOSM implementation of the {@link java.text.DecimalFormatSymbols DecimalFormatSymbols} class,
+ * consistent with ISO 80000-1.
+ * This class will only be used with Java 9 and later runtimes, as Java 8 implementation relies
+ * on Java Extension Mechanism only, while Java 9 supports application classpath.
+ * See {@link java.util.spi.LocaleServiceProvider LocaleServiceProvider} javadoc for more details.
+ * @since 12931
+ */
+public class JosmDecimalFormatSymbolsProvider extends DecimalFormatSymbolsProvider {
+
+    @Override
+    public DecimalFormatSymbols getInstance(Locale locale) {
+        DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
+        // Override digit group separator to be consistent across languages with ISO 80000-1, chapter 7.3.1
+        symbols.setGroupingSeparator('\u202F'); // U+202F: NARROW NO-BREAK SPACE
+        return symbols;
+    }
+
+    @Override
+    public Locale[] getAvailableLocales() {
+        return I18n.getAvailableTranslations();
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/tools/Logging.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/Logging.java	(revision 12930)
+++ /trunk/src/org/openstreetmap/josm/tools/Logging.java	(revision 12931)
@@ -52,4 +52,8 @@
 
     static {
+        // We need to be sure java.locale.providers system property is initialized by JOSM, not by JRE
+        // The call to ConsoleHandler constructor makes the JRE access this property by side effect
+        I18n.setupJavaLocaleProviders();
+
         LOGGER.setLevel(Level.ALL);
         LOGGER.setUseParentHandlers(false);
