88541dae022f82687244bc4c64eb54ebe8ff9036
[idea/community.git] / platform / util / src / com / intellij / BundleBase.java
1 // Copyright 2000-2020 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.
2 package com.intellij;
3
4 import com.intellij.openapi.diagnostic.Logger;
5 import com.intellij.openapi.util.NlsSafe;
6 import com.intellij.openapi.util.SystemInfoRt;
7 import com.intellij.util.ReflectionUtil;
8 import com.intellij.util.text.OrdinalFormat;
9 import org.jetbrains.annotations.Contract;
10 import org.jetbrains.annotations.Nls;
11 import org.jetbrains.annotations.NotNull;
12 import org.jetbrains.annotations.Nullable;
13
14 import java.lang.reflect.Field;
15 import java.text.MessageFormat;
16 import java.util.Locale;
17 import java.util.MissingResourceException;
18 import java.util.ResourceBundle;
19
20 /**
21  * @author yole
22  */
23 public abstract class BundleBase {
24   public static final char MNEMONIC = 0x1B;
25   public static final String MNEMONIC_STRING = Character.toString(MNEMONIC);
26   static final String L10N_MARKER = "🔅";
27   public static final boolean SHOW_LOCALIZED_MESSAGES = Boolean.getBoolean("idea.l10n");
28   public static final boolean SHOW_DEFAULT_MESSAGES = Boolean.getBoolean("idea.l10n.english");
29   public static final boolean SHOW_KEYS = Boolean.getBoolean("idea.l10n.keys");
30   private static final Logger LOG = Logger.getInstance(BundleBase.class);
31
32   private static boolean assertOnMissedKeys;
33
34   public static void assertOnMissedKeys(boolean doAssert) {
35     assertOnMissedKeys = doAssert;
36   }
37
38   /**
39    * Performs partial application of the pattern message from the bundle leaving some parameters unassigned.
40    * It's expected that the message contains params.length+unassignedParams placeholders. Parameters
41    * {@code {0}..{params.length-1}} will be substituted using passed params array. The remaining parameters
42    * will be renumbered: {@code {params.length}} will become {@code {0}} and so on, so the resulting template
43    * could be applied once more.
44    * 
45    * @param bundle resource bundle to find the message in
46    * @param key resource key
47    * @param unassignedParams number of unassigned parameters
48    * @param params assigned parameters
49    * @return a template suitable to pass to {@link MessageFormat#format(Object)} having the specified number of placeholders left
50    */
51   public static @Nls String partialMessage(@NotNull ResourceBundle bundle,
52                                            @NotNull String key,
53                                            int unassignedParams,
54                                            Object @NotNull ... params) {
55     if (unassignedParams <= 0) throw new IllegalArgumentException();
56     Object[] newParams = new Object[params.length + unassignedParams];
57     System.arraycopy(params, 0, newParams, 0, params.length);
58     final String prefix = "#$$$TemplateParameter$$$#";
59     final String suffix = "#$$$/TemplateParameter$$$#";
60     for (int i = 0; i < unassignedParams; i++) {
61       newParams[i + params.length] = prefix + i + suffix;
62     }
63     String message = message(bundle, key, newParams);
64     return quotePattern(message).replace(prefix, "{").replace(suffix, "}"); //NON-NLS
65   }
66
67   private static String quotePattern(String message) {
68     boolean inQuotes = false;
69     StringBuilder sb = new StringBuilder(message.length()+5);
70     for (int i = 0; i < message.length(); i++) {
71       char c = message.charAt(i);
72       boolean needToQuote = c == '{' || c == '}';
73       if (needToQuote != inQuotes) {
74         inQuotes = needToQuote;
75         sb.append('\'');
76       }
77       if (c == '\'') {
78         sb.append("''");
79       } else {
80         sb.append(c);
81       }
82     }
83     if (inQuotes) {
84       sb.append('\'');
85     }
86     return sb.toString();
87   }
88
89   @NotNull
90   public static @Nls String message(@NotNull ResourceBundle bundle, @NotNull String key, Object @NotNull ... params) {
91     return messageOrDefault(bundle, key, null, params);
92   }
93
94   public static @Nls String messageOrDefault(@Nullable ResourceBundle bundle,
95                                              @NotNull String key,
96                                              @Nullable String defaultValue,
97                                              Object @NotNull ... params) {
98     if (bundle == null) return defaultValue;
99
100     boolean resourceFound = true;
101     
102     String value;
103     try {
104       value = bundle.getString(key);
105     }
106     catch (MissingResourceException e) {
107       resourceFound = false;
108       value = useDefaultValue(bundle, key, defaultValue);
109     }
110
111     String result = postprocessValue(bundle, value, params);
112
113     if (!resourceFound) {
114       return result;
115     }
116
117     if (SHOW_KEYS && SHOW_DEFAULT_MESSAGES) {
118       return appendLocalizationSuffix(result, " (" + key + "=" + getDefaultMessage(bundle, key) + ")");
119     }
120     if (SHOW_KEYS) {
121       return appendLocalizationSuffix(result, " (" + key + ")");
122     }
123     if (SHOW_DEFAULT_MESSAGES) {
124       return appendLocalizationSuffix(result, " (" + getDefaultMessage(bundle, key) + ")");
125     }
126     if (SHOW_LOCALIZED_MESSAGES) {
127       return appendLocalizationSuffix(result, L10N_MARKER);
128     }
129     return result;
130   }
131
132   @NotNull
133   public static String getDefaultMessage(@NotNull ResourceBundle bundle, @NotNull String key) {
134     try {
135       Field parent = ReflectionUtil.getDeclaredField(ResourceBundle.class, "parent");
136       if (parent != null) {
137         Object parentBundle = parent.get(bundle);
138         if (parentBundle instanceof ResourceBundle) {
139           return ((ResourceBundle)parentBundle).getString(key);
140         }
141       }
142     }
143     catch (IllegalAccessException e) {
144       LOG.warn("Cannot fetch default message with -Didea.l10n.english enabled, by key '" + key + "'");
145     }
146     return "undefined";
147   }
148
149   private static final String[] SUFFIXES = {"</body></html>", "</html>"};
150
151   @NotNull
152   protected static @NlsSafe String appendLocalizationSuffix(@NotNull String result, @NotNull String suffixToAppend) {
153     for (String suffix : SUFFIXES) {
154       if (result.endsWith(suffix)) return result.substring(0, result.length() - suffix.length()) + L10N_MARKER + suffix;
155     }
156     return result + suffixToAppend;
157   }
158
159   @NotNull
160   static String useDefaultValue(@Nullable ResourceBundle bundle, @NotNull String key, @Nullable String defaultValue) {
161     if (defaultValue != null) {
162       return defaultValue;
163     }
164
165     if (assertOnMissedKeys) {
166       LOG.error("'" + key + "' is not found in " + bundle);
167     }
168     return "!" + key + "!";
169   }
170
171   @NotNull
172   static @Nls String postprocessValue(@NotNull ResourceBundle bundle, @NotNull String value, Object @NotNull ... params) {
173     value = replaceMnemonicAmpersand(value);
174
175     if (params.length > 0 && value.indexOf('{') >= 0) {
176       Locale locale = bundle.getLocale();
177       try {
178         MessageFormat format = locale != null ? new MessageFormat(value, locale) : new MessageFormat(value);
179         OrdinalFormat.apply(format);
180         value = format.format(params);
181       }
182       catch (IllegalArgumentException e) {
183         value = "!invalid format: `" + value + "`!";
184       }
185     }
186
187     return value;
188   }
189
190   @NotNull
191   public static String format(@NotNull String value, Object @NotNull ... params) {
192     return params.length > 0 && value.indexOf('{') >= 0 ? MessageFormat.format(value, params) : value;
193   }
194
195   @Contract("null -> null; !null -> !null")
196   public static @Nls String replaceMnemonicAmpersand(@Nullable @Nls String value) {
197     if (value == null || value.indexOf('&') < 0) {
198       return value;
199     }
200
201     StringBuilder builder = new StringBuilder();
202     boolean macMnemonic = value.contains("&&");
203     int i = 0;
204     while (i < value.length()) {
205       char c = value.charAt(i);
206       if (c == '\\') {
207         if (i < value.length() - 1 && value.charAt(i + 1) == '&') {
208           builder.append('&');
209           i++;
210         }
211         else {
212           builder.append(c);
213         }
214       }
215       else if (c == '&') {
216         if (i < value.length() - 1 && value.charAt(i + 1) == '&') {
217           if (SystemInfoRt.isMac) {
218             builder.append(MNEMONIC);
219           }
220           i++;
221         }
222         else if (!SystemInfoRt.isMac || !macMnemonic) {
223           builder.append(MNEMONIC);
224         }
225       }
226       else {
227         builder.append(c);
228       }
229       i++;
230     }
231     return builder.toString();
232   }
233 }