diff --git a/platform/util-rt/src/com/intellij/BundleBase.java b/platform/util-rt/src/com/intellij/BundleBase.java index c91595cca818..be81b10123fd 100644 --- a/platform/util-rt/src/com/intellij/BundleBase.java +++ b/platform/util-rt/src/com/intellij/BundleBase.java @@ -2,10 +2,12 @@ package com.intellij; import com.intellij.openapi.util.SystemInfoRt; +import com.intellij.util.text.OrdinalFormat; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.text.MessageFormat; +import java.util.Locale; import java.util.MissingResourceException; import java.util.ResourceBundle; @@ -18,9 +20,9 @@ public abstract class BundleBase { public static boolean assertKeyIsFound = false; - public static String messageOrDefault(@Nullable final ResourceBundle bundle, + public static String messageOrDefault(@Nullable ResourceBundle bundle, @NotNull String key, - @Nullable final String defaultValue, + @Nullable String defaultValue, @NotNull Object... params) { if (bundle == null) return defaultValue; @@ -42,7 +44,14 @@ public abstract class BundleBase { value = replaceMnemonicAmpersand(value); - return format(value, params); + if (params.length > 0 && value.indexOf('{') >= 0) { + Locale locale = bundle.getLocale(); + MessageFormat format = locale != null ? new MessageFormat(value, locale) : new MessageFormat(value); + OrdinalFormat.apply(format); + value = format.format(params); + } + + return value; } @NotNull diff --git a/platform/util-rt/src/com/intellij/util/text/OrdinalFormat.java b/platform/util-rt/src/com/intellij/util/text/OrdinalFormat.java new file mode 100644 index 000000000000..331205018555 --- /dev/null +++ b/platform/util-rt/src/com/intellij/util/text/OrdinalFormat.java @@ -0,0 +1,62 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.util.text; + +import java.text.*; +import java.util.Locale; + +public class OrdinalFormat { + /** + * Replaces all instances of {@code "{?,number,ordinal}"} format elements with the ordinal format for the locale. + */ + public static void apply(MessageFormat format) { + Format[] formats = format.getFormats(); + NumberFormat ordinal = null; + for (int i = 0; i < formats.length; i++) { + Format element = formats[i]; + if (element instanceof DecimalFormat && "ordinal".equals(((DecimalFormat)element).getPositivePrefix())) { + if (ordinal == null) ordinal = getOrdinalFormat(format.getLocale()); + format.setFormat(i, ordinal); + } + } + } + + private static NumberFormat getOrdinalFormat(Locale locale) { + if (locale != null) { + String language = locale.getLanguage(); + if ("en".equals(language) || + language != null && language.length() == 0 /*bundle fallback locale*/) { + return new EnglishOrdinalFormat(); + } + } + + return new DecimalFormat(); + } + + private static class EnglishOrdinalFormat extends NumberFormat { + @Override + public StringBuffer format(long number, StringBuffer toAppendTo, FieldPosition pos) { + return new MessageFormat("{0}").format(new Object[]{formatEnglish(number)}, toAppendTo, pos); + } + + @Override + public StringBuffer format(double number, StringBuffer toAppendTo, FieldPosition pos) { + throw new IllegalArgumentException("Cannot format non-integer number"); + } + + @Override + public Number parse(String source, ParsePosition parsePosition) { + throw new UnsupportedOperationException(); + } + } + + public static String formatEnglish(long num) { + long mod = Math.abs(num) % 100; + if (mod < 11 || mod > 13) { + mod = mod % 10; + if (mod == 1) return num + "st"; + if (mod == 2) return num + "nd"; + if (mod == 3) return num + "rd"; + } + return num + "th"; + } +} \ No newline at end of file diff --git a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java index 03ce289f9ee5..aa4fefd46ed2 100644 --- a/platform/util/src/com/intellij/openapi/util/text/StringUtil.java +++ b/platform/util/src/com/intellij/openapi/util/text/StringUtil.java @@ -8,10 +8,7 @@ import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; import com.intellij.util.*; import com.intellij.util.containers.ContainerUtil; -import com.intellij.util.text.CharArrayUtil; -import com.intellij.util.text.CharSequenceSubSequence; -import com.intellij.util.text.MergingCharSequence; -import com.intellij.util.text.StringFactory; +import com.intellij.util.text.*; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -25,6 +22,7 @@ import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.*; +import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -1634,23 +1632,11 @@ public class StringUtil extends StringUtilRt { return sb.toString(); } - public static String formatOrdinal(int i) { - int mod = i % 100; - if (mod == 11 || mod == 12 || mod == 13) { - return i + "th"; - } - - mod = mod % 10; - if (mod == 1) { - return i + "st"; - } - else if (mod == 2) { - return i + "nd"; - } - else if (mod == 3) { - return i + "rd"; - } - return i + "th"; + /** + * Appends English ordinal suffix to the given number. + */ + public static String formatOrdinal(long num) { + return OrdinalFormat.formatEnglish(num); } /** diff --git a/platform/util/testSrc/com/intellij/util/text/OrdinalFormatTest.java b/platform/util/testSrc/com/intellij/util/text/OrdinalFormatTest.java new file mode 100644 index 000000000000..185d57b1ccc7 --- /dev/null +++ b/platform/util/testSrc/com/intellij/util/text/OrdinalFormatTest.java @@ -0,0 +1,40 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.util.text; + +import com.intellij.BundleBase; +import org.junit.Test; + +import java.util.ListResourceBundle; +import java.util.Locale; +import java.util.ResourceBundle; + +import static org.junit.Assert.assertEquals; + +public class OrdinalFormatTest { + @Test + public void locales() { + // the language bundle is present, but the language is not supported by the formatter - should fall back to plain decimal + ResourceBundle fr = ResourceBundle.getBundle("com.intellij.util.text.OrdinalFormatTest$TestBundle", Locale.FRENCH); + assertEquals("Blah blah 1 blah blah 'whatever' blah blah 33 [fr]", BundleBase.message(fr, "ordinals", 1, "whatever", 33)); + + // the language bundle isn't present - should fall back to the default (i.e. English) + ResourceBundle de = ResourceBundle.getBundle("com.intellij.util.text.OrdinalFormatTest$TestBundle", Locale.GERMAN); + assertEquals("Blah blah 1st blah blah 'whatever' blah blah 33rd [en]", BundleBase.message(de, "ordinals", 1, "whatever", 33)); + } + + @SuppressWarnings("unused") + public static class TestBundle extends ListResourceBundle { + @Override + protected Object[][] getContents() { + return new Object[][]{{"ordinals", "Blah blah {0,number,ordinal} blah blah ''{1}'' blah blah {2,number,ordinal} [en]"}}; + } + } + + @SuppressWarnings("unused") + public static class TestBundle_fr extends ListResourceBundle { + @Override + protected Object[][] getContents() { + return new Object[][]{{"ordinals", "Blah blah {0,number,ordinal} blah blah ''{1}'' blah blah {2,number,ordinal} [fr]"}}; + } + } +} \ No newline at end of file diff --git a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java index ed1c03b58789..5e2a2af1619b 100644 --- a/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java +++ b/platform/util/testSrc/com/intellij/util/text/StringUtilTest.java @@ -768,4 +768,15 @@ public class StringUtilTest { assertTrue(String.valueOf(last.getClass()), last instanceof CharSequenceSubSequence); assertEquals("t", last.toString()); } + + @Test + public void testEnglishOrdinals() { + assertEquals("100th", StringUtil.formatOrdinal(100)); + assertEquals("101st", StringUtil.formatOrdinal(101)); + assertEquals("111th", StringUtil.formatOrdinal(111)); + assertEquals("122nd", StringUtil.formatOrdinal(122)); + + assertEquals("-3rd", StringUtil.formatOrdinal(-3)); + assertEquals("-9223372036854775808th", StringUtil.formatOrdinal(Long.MIN_VALUE)); + } } \ No newline at end of file