View Javadoc
1   /**
2    * This file Copyright (c) 2013-2017 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.i18nsystem;
35  
36  import info.magnolia.cms.i18n.MessagesManager;
37  import info.magnolia.event.EventBus;
38  import info.magnolia.event.SystemEventBus;
39  import info.magnolia.i18nsystem.module.I18nModule;
40  import info.magnolia.objectfactory.ComponentProvider;
41  import info.magnolia.objectfactory.Components;
42  import info.magnolia.resourceloader.ResourceOrigin;
43  
44  import java.util.Arrays;
45  import java.util.Locale;
46  import java.util.Properties;
47  
48  import javax.inject.Inject;
49  import javax.inject.Named;
50  import javax.inject.Provider;
51  import javax.inject.Singleton;
52  
53  import org.apache.commons.lang3.StringUtils;
54  import org.jsoup.Jsoup;
55  import org.jsoup.safety.Whitelist;
56  import org.slf4j.Logger;
57  import org.slf4j.LoggerFactory;
58  
59  /**
60   * A TranslationService which relies on a "global" message bundle, as loaded per {@link DefaultMessageBundlesLoader}.
61   * Each given key is looked up in the given locale, then in less specific locales, then in the fallback locale.
62   * That is, if [a, b, c] are passed as keys, with country-specific Locale such as de_CH, and "b" is defined in the de_CH
63   * message bundle, it will be returned, even if "a" is defined in the less specific "de" Locale.
64   *
65   * When a basename is explicitly specified, it however uses {@link MessagesManager} to look the keys up, falls back
66   * to the default bundle of MessagesManager, then finally falls back on its own global message bundle.
67   */
68  @Singleton
69  public class TranslationServiceImpl implements TranslationService {
70      private static final Logger log = LoggerFactory.getLogger(TranslationServiceImpl.class);
71  
72      private final Provider<I18nModule> i18nModuleProvider;
73      private final Provider<DefaultMessageBundlesLoader> defaultMessageBundlesLoaderProvider;
74  
75      @Inject
76      public TranslationServiceImpl(final Provider<I18nModule> i18nModuleProvider, final Provider<DefaultMessageBundlesLoader> defaultMessageBundlesLoaderProvider) {
77          this.i18nModuleProvider = i18nModuleProvider;
78          this.defaultMessageBundlesLoaderProvider = defaultMessageBundlesLoaderProvider;
79      }
80  
81      /**
82       * @deprecated since 5.5.2. Use {@link #TranslationServiceImpl(javax.inject.Provider, javax.inject.Provider<DefaultMessageBundlesLoader>)} instead.
83       */
84      @Deprecated
85      public TranslationServiceImpl(final Provider<I18nModule> i18nModuleProvider, final ComponentProvider componentProvider, final ResourceOrigin resourceOrigin, @Named(SystemEventBus.NAME) EventBus systemEventBus) {
86          this(i18nModuleProvider, new Provider<DefaultMessageBundlesLoader>() {
87              @Override
88              public DefaultMessageBundlesLoader get() {
89                  return Components.getComponent(DefaultMessageBundlesLoader.class);
90              }
91          });
92      }
93  
94      /**
95       * @deprecated since 5.4.5. Use {@link #TranslationServiceImpl(javax.inject.Provider, javax.inject.Provider<DefaultMessageBundlesLoader>)} instead.
96       */
97      @Deprecated
98      public TranslationServiceImpl(Provider<I18nModule> i18nModuleProvider) {
99          this(i18nModuleProvider, new Provider<DefaultMessageBundlesLoader>() {
100             @Override
101             public DefaultMessageBundlesLoader get() {
102                 return Components.getComponent(DefaultMessageBundlesLoader.class);
103             }
104         });
105     }
106 
107     /**
108      * @deprecated since 5.4.4. Use {@link #TranslationServiceImpl(javax.inject.Provider, javax.inject.Provider<DefaultMessageBundlesLoader>)} instead.
109      */
110     @Deprecated
111     public TranslationServiceImpl() {
112         this(new Provider<I18nModule>() {
113             @Override
114             public I18nModule get() {
115                 return Components.getComponent(I18nModule.class);
116             }
117         }, new Provider<DefaultMessageBundlesLoader>() {
118             @Override
119             public DefaultMessageBundlesLoader get() {
120                 return Components.getComponent(DefaultMessageBundlesLoader.class);
121             }
122         });
123     }
124 
125     /**
126      * @deprecated since 5.5.2. Use {@link #defaultMessageBundlesLoaderProvider} directly or setup message bundles in {@link DefaultMessageBundlesLoader}.
127      */
128     @Deprecated
129     protected DefaultMessageBundlesLoader setupMessageBundles() {
130         return defaultMessageBundlesLoaderProvider.get();
131     }
132 
133     @Override
134     public String translate(LocaleProvider localeProvider, String[] keys) {
135         return translate(localeProvider, null, keys);
136     }
137 
138     @Override
139     public String translate(LocaleProvider localeProvider, String basename, String[] keys) {
140         final Locale locale = localeProvider.getLocale();
141         if (locale == null) {
142             throw new IllegalArgumentException("Locale can't be null");
143         }
144         if (keys == null || keys.length < 1) {
145             throw new IllegalArgumentException("Keys can't be null or empty");
146         }
147 
148         if (basename != null) {
149             log.debug("Got an explicit basename ({}) for keys {}", basename, Arrays.asList(keys));
150         }
151 
152         final String message = lookUpKeyUntilFound(keys, locale, basename);
153         if (message != null) {
154             return message;
155         } else {
156             return handleUnknownKey(locale, basename, keys);
157         }
158     }
159 
160     private String lookUpKeyUntilFound(final String[] keys, final Locale locale, final String basename) {
161         String message = null;
162 
163         if (StringUtils.isNotBlank(basename)) {
164             log.debug("Looking up key [{}] with basename [{}] and Locale [{}] - legacy method", keys[0], basename, locale);
165             message = legacyLookup(locale, basename, keys[0]);
166             if (message != null) {
167                 return message + (isDebug() ? this.addDebugInfo(keys, keys[0], locale, basename) : StringUtils.EMPTY);
168             }
169         }
170 
171         if (message == null) {
172             log.debug("Looking up in global i18n message bundle with key [{}] and Locale [{}]", Arrays.asList(keys), locale);
173             message = doGetMessage(keys, locale);
174         }
175 
176         if (message == null) {
177             final String country = locale.getCountry();
178             if (country != null) {
179                 final Locale newLocale = new Locale(locale.getLanguage(), country);
180                 message = doGetMessage(keys, newLocale);
181             }
182         }
183 
184         if (message == null) {
185             final Locale newLocale = new Locale(locale.getLanguage());
186             message = doGetMessage(keys, newLocale);
187         }
188 
189         if (message == null) {
190             message = doGetMessage(keys, getFallbackLocale());
191         }
192 
193         if (message == null) {
194             message = handleUnknownKey(locale, basename, keys);
195         }
196 
197         return message;
198     }
199 
200     private String addDebugInfo(final String[] keys, String currentlyUsedKey, final Locale locale, final String basename) {
201         return "\n" + Arrays.asList(keys).toString()
202                 .replaceFirst("(?s)" + currentlyUsedKey + "(?!.*?" + currentlyUsedKey + ")", ">" + currentlyUsedKey + "<") //replace last occurrence
203                 + locale.getClass().getSimpleName() + ":" + locale + (basename == null ? StringUtils.EMPTY : ",Using legacy i18n basename:" + basename); //: is used because = is stripped in area/components bars in page editor
204     }
205 
206     protected String handleUnknownKey(Locale locale, String basename, String[] keys) {
207         // TODO - this method could be context dependent, or delegate to a configured component. In dev mode, for instance, we could at least print this out, or return the key, while in production this is neither useful nor needed.
208         log.debug("No translation found for any of {} with locale {} and basename {}", keys, locale, basename != null ? basename : "<unspecified>");
209         return keys[0] + (isDebug() ? this.addDebugInfo(keys, null, locale, basename) : StringUtils.EMPTY);
210     }
211 
212     /**
213      * Looks up a particular key using the given basename and Locale, using the legacy MessagesManager.
214      */
215     private String legacyLookup(Locale locale, String basename, String key) {
216         // Note that this internally chains the given locale with the fallback locale (as known by the MessagesManager), so if a key is known in english, it will be returned in english before we lookup in the default bundles
217         String message = MessagesManager.getMessages(basename, locale).get(key);
218         if (legacyMessageNotFound(message)) {
219             message = MessagesManager.getMessages(MessagesManager.DEFAULT_BASENAME, locale).get(key);
220         }
221         if (legacyMessageNotFound(message)) {
222             // Let's not get any of the legacy "???" markers out of here
223             return null;
224         } else {
225             if (!Jsoup.isValid(message, Whitelist.basic())) {
226                 return Jsoup.clean(message, Whitelist.basic());
227             } else {
228                 return message;
229             }
230         }
231     }
232 
233     private boolean legacyMessageNotFound(final String message) {
234         return message == null || message.startsWith("???");
235     }
236 
237     private String doGetMessage(String[] keys, Locale locale) {
238         final Properties properties = defaultMessageBundlesLoaderProvider.get().getMessages().get(locale);
239         if (properties != null) {
240             for (String key : keys) {
241                 if (key == null) {
242                     // Keys can sometimes be null (e.g when using NullKeyGenerator and the undecorated or configured text is null)
243                     continue;
244                 }
245                 final String message = properties.getProperty(key);
246                 if (message != null) {
247                     return message + (isDebug() ? this.addDebugInfo(keys, key, locale, null) : StringUtils.EMPTY);
248                 }
249             }
250         }
251         return null;
252     }
253 
254     private boolean isDebug() {
255         try {
256             return i18nModuleProvider.get() != null && i18nModuleProvider.get().isDebug();
257         } catch (RuntimeException exception) {
258             // Under certain circumstances the provider will throw this exception (e.g. when module is not loaded yet).
259             // We are catching it as we only need to know whether we want debugging on or not even though the javadoc
260             // of javax.inject.Provider.get() suggests to not handle these.
261             return false;
262         }
263     }
264 
265     // TODO - move this to a SystemLocalesManager component
266     private Locale getFallbackLocale() {
267         return MessagesManager.getInstance().getDefaultLocale();
268     }
269 
270     /**
271      * @deprecated since 5.5.2. Reload message bundles in {@link DefaultMessageBundlesLoader} directly.
272      */
273     @Deprecated
274     @Override
275     public void reloadMessageBundles() {
276         log.warn("Not reloading message bundles. Please make sure to update [{}] instead.", DefaultMessageBundlesLoader.class.getName());
277     }
278 
279 }