View Javadoc
1   /**
2    * This file Copyright (c) 2013-2016 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.module.ModulesStartedEvent;
41  import info.magnolia.objectfactory.ComponentProvider;
42  import info.magnolia.objectfactory.Components;
43  import info.magnolia.resourceloader.Resource;
44  import info.magnolia.resourceloader.ResourceOrigin;
45  import info.magnolia.resourceloader.util.PredicatedResourceVisitor;
46  import info.magnolia.resourceloader.util.VoidFunction;
47  
48  import java.util.Arrays;
49  import java.util.Locale;
50  import java.util.Properties;
51  
52  import javax.inject.Inject;
53  import javax.inject.Named;
54  import javax.inject.Provider;
55  import javax.inject.Singleton;
56  
57  import org.apache.commons.lang3.StringUtils;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  /**
62   * A TranslationService which relies on a "global" message bundle, as loaded per {@link DefaultMessageBundlesLoader}.
63   * Each given key is looked up in the given locale, then in less specific locales, then in the fallback locale.
64   * 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
65   * message bundle, it will be returned, even if "a" is defined in the less specific "de" Locale.
66   *
67   * When a basename is explicitly specified, it however uses {@link MessagesManager} to look the keys up, falls back
68   * to the default bundle of MessagesManager, then finally falls back on its own global message bundle.
69   */
70  @Singleton
71  public class TranslationServiceImpl implements TranslationService {
72      private static final Logger log = LoggerFactory.getLogger(TranslationServiceImpl.class);
73  
74      private final ResourceOrigin resourceOrigin;
75      private DefaultMessageBundlesLoader messageBundles;
76  
77      private final Provider<I18nModule> i18nModuleProvider;
78      private final ComponentProvider componentProvider;
79  
80      @Inject
81      public TranslationServiceImpl(final Provider<I18nModule> i18nModuleProvider,final ComponentProvider componentProvider, final ResourceOrigin resourceOrigin, @Named(SystemEventBus.NAME) EventBus systemEventBus) {
82          this.i18nModuleProvider = i18nModuleProvider;
83          this.componentProvider = componentProvider;
84          this.resourceOrigin = resourceOrigin;
85  
86          systemEventBus.addHandler(ModulesStartedEvent.class, new ModulesStartedEvent.Handler() {
87              @Override
88              public void onModuleStartupCompleted(ModulesStartedEvent event) {
89                  messageBundles = setupMessageBundles();
90                  log.info("Starting monitoring of {} to load translation files", resourceOrigin);
91                  resourceOrigin.watchForChanges(PredicatedResourceVisitor.with(DefaultMessageBundlesLoader.DIRECTORY_PREDICATE, DefaultMessageBundlesLoader.RESOURCE_PREDICATE, new VoidFunction<Resource>() {
92                      @Override
93                      public void doWith(Resource input) {
94                          reloadMessageBundles();
95                      }
96                  }));
97              }
98          });
99      }
100 
101     /**
102      * @deprecated since 5.4.5. Use {@link #TranslationServiceImpl(javax.inject.Provider, info.magnolia.objectfactory.ComponentProvider, info.magnolia.resourceloader.ResourceOrigin, info.magnolia.event.EventBus)}  instead.
103      */
104     @Deprecated
105     public TranslationServiceImpl(Provider<I18nModule> i18nModuleProvider) {
106         this(i18nModuleProvider, Components.getComponentProvider(), Components.getComponent(ResourceOrigin.class), Components.getComponentWithAnnotation(EventBus.class, Components.named(SystemEventBus.NAME)));
107     }
108 
109     /**
110      * @deprecated since 5.4.4. Use {@link #TranslationServiceImpl(javax.inject.Provider, info.magnolia.objectfactory.ComponentProvider, info.magnolia.resourceloader.ResourceOrigin, info.magnolia.event.EventBus)}  instead.
111      */
112     @Deprecated
113     public TranslationServiceImpl() {
114         this(new Provider<I18nModule>() {
115             @Override
116             public I18nModule get() {
117                 return Components.getComponent(I18nModule.class);
118             }
119         }, Components.getComponentProvider(), Components.getComponent(ResourceOrigin.class), Components.getComponentWithAnnotation(EventBus.class, Components.named(SystemEventBus.NAME)));
120     }
121 
122     protected DefaultMessageBundlesLoader setupMessageBundles() {
123         return componentProvider.newInstance(DefaultMessageBundlesLoader.class, resourceOrigin);
124     }
125 
126     @Override
127     public String translate(LocaleProvider localeProvider, String[] keys) {
128         return translate(localeProvider, null, keys);
129     }
130 
131     @Override
132     public String translate(LocaleProvider localeProvider, String basename, String[] keys) {
133         final Locale locale = localeProvider.getLocale();
134         if (locale == null) {
135             throw new IllegalArgumentException("Locale can't be null");
136         }
137         if (keys == null || keys.length < 1) {
138             throw new IllegalArgumentException("Keys can't be null or empty");
139         }
140 
141         if (basename != null) {
142             log.debug("Got an explicit basename ({}) for keys {}", basename, Arrays.asList(keys));
143         }
144 
145         final String message = lookUpKeyUntilFound(keys, locale, basename);
146         if (message != null) {
147             return message;
148         } else {
149             return handleUnknownKey(locale, basename, keys);
150         }
151     }
152 
153     private String lookUpKeyUntilFound(final String[] keys, final Locale locale, final String basename) {
154         String message = null;
155 
156         if (StringUtils.isNotBlank(basename)) {
157             log.debug("Looking up key [{}] with basename [{}] and Locale [{}] - legacy method", keys[0], basename, locale);
158             message = legacyLookup(locale, basename, keys[0]);
159             if (message != null) {
160                 return message + (i18nModuleProvider.get().isDebug() ? this.addDebugInfo(keys, keys[0], locale, basename) : StringUtils.EMPTY);
161             }
162         }
163 
164         if (message == null) {
165             log.debug("Looking up in global i18n message bundle with key [{}] and Locale [{}]", Arrays.asList(keys), locale);
166             message = doGetMessage(keys, locale);
167         }
168 
169         if (message == null) {
170             final String country = locale.getCountry();
171             if (country != null) {
172                 final Locale newLocale = new Locale(locale.getLanguage(), country);
173                 message = doGetMessage(keys, newLocale);
174             }
175         }
176 
177         if (message == null) {
178             final Locale newLocale = new Locale(locale.getLanguage());
179             message = doGetMessage(keys, newLocale);
180         }
181 
182         if (message == null) {
183             message = doGetMessage(keys, getFallbackLocale());
184         }
185 
186         if (message == null) {
187             message = handleUnknownKey(locale, basename, keys);
188         }
189 
190         return message;
191     }
192 
193     private String addDebugInfo(final String[] keys, String currentlyUsedKey, final Locale locale, final String basename) {
194         return "\n" + Arrays.asList(keys).toString()
195                 .replaceFirst("(?s)" + currentlyUsedKey + "(?!.*?" + currentlyUsedKey + ")", ">" + currentlyUsedKey + "<") //replace last occurrence
196                 + 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
197     }
198 
199     protected String handleUnknownKey(Locale locale, String basename, String[] keys) {
200         // 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.
201         log.debug("No translation found for any of {} with locale {} and basename {}", keys, locale, basename != null ? basename : "<unspecified>");
202         return keys[0] + (i18nModuleProvider.get().isDebug() ? this.addDebugInfo(keys, null, locale, basename) : StringUtils.EMPTY);
203     }
204 
205     /**
206      * Looks up a particular key using the given basename and Locale, using the legacy MessagesManager.
207      */
208     private String legacyLookup(Locale locale, String basename, String key) {
209         // 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
210         String message = MessagesManager.getMessages(basename, locale).get(key);
211         if (legacyMessageNotFound(message)) {
212             message = MessagesManager.getMessages(MessagesManager.DEFAULT_BASENAME, locale).get(key);
213         }
214         if (legacyMessageNotFound(message)) {
215             // Let's not get any of the legacy "???" markers out of here
216             return null;
217         } else {
218             return message;
219         }
220     }
221 
222     private boolean legacyMessageNotFound(final String message) {
223         return message == null || message.startsWith("???");
224     }
225 
226     private String doGetMessage(String[] keys, Locale locale) {
227         final Properties properties = messageBundles != null && messageBundles.getMessages() != null ? messageBundles.getMessages().get(locale) : null;
228         if (properties != null) {
229             for (String key : keys) {
230                 final String message = properties.getProperty(key);
231                 if (message != null) {
232                     return message + (i18nModuleProvider.get().isDebug() ? this.addDebugInfo(keys, key, locale, null) : StringUtils.EMPTY);
233                 }
234             }
235         }
236         return null;
237     }
238 
239     // TODO - move this to a SystemLocalesManager component
240     private Locale getFallbackLocale() {
241         return MessagesManager.getInstance().getDefaultLocale();
242     }
243 
244     @Override
245     public void reloadMessageBundles() {
246         log.info("Reloading message bundles");
247         this.messageBundles = setupMessageBundles();
248     }
249 
250 }