001/*
002 * ============================================================================
003 * Copyright © 2002-2024 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 *
007 * Licensed to the public under the agreements of the GNU Lesser General Public
008 * License, version 3.0 (the "License"). You may obtain a copy of the License at
009 *
010 *      http://www.gnu.org/licenses/lgpl.html
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015 * License for the specific language governing permissions and limitations
016 * under the License.
017 */
018
019package org.tquadrat.foundation.i18n;
020
021import static java.lang.String.format;
022import static java.lang.System.setProperty;
023import static org.apiguardian.api.API.Status.INTERNAL;
024import static org.apiguardian.api.API.Status.STABLE;
025import static org.tquadrat.foundation.i18n.TextUse.STRING;
026import static org.tquadrat.foundation.lang.CommonConstants.ISO8859_1;
027import static org.tquadrat.foundation.lang.CommonConstants.PROPERTY_RESOURCEBUNDLE_ENCODING;
028import static org.tquadrat.foundation.lang.DebugOutput.ifDebug;
029import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
030import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
031import static org.tquadrat.foundation.util.StringUtils.isNotEmptyOrBlank;
032
033import java.util.MissingResourceException;
034import java.util.Optional;
035import java.util.ResourceBundle;
036
037import org.apiguardian.api.API;
038import org.tquadrat.foundation.annotation.ClassVersion;
039import org.tquadrat.foundation.annotation.UtilityClass;
040import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError;
041import org.tquadrat.foundation.lang.Objects;
042
043/**
044 *  Utilities that are related to the i18n feature.
045 *
046 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
047 *  @version $Id: I18nUtil.java 1085 2024-01-05 16:23:28Z tquadrat $
048 *  @since 0.1.0
049 *
050 *  @UMLGraph.link
051 */
052@UtilityClass
053@ClassVersion( sourceVersion = "$Id: I18nUtil.java 1085 2024-01-05 16:23:28Z tquadrat $" )
054@API( status = STABLE, since = "0.1.0" )
055public final class I18nUtil
056{
057        /*-----------*\
058    ====** Constants **========================================================
059        \*-----------*/
060    /**
061     *  The name for the file with the additional texts: {@value}.
062     */
063    @API( status = STABLE, since = "0.1.0" )
064    public static final String ADDITIONAL_TEXT_FILE = "AdditionalTexts.xml";
065
066    /**
067     *  The name of the annotation processor option that provides the location
068     *  for the file with the additional texts
069     *  ({@value org.tquadrat.foundation.i18n.I18nUtil#ADDITIONAL_TEXT_FILE}):
070     *  {@value}.
071     */
072    @API( status = STABLE, since = "0.1.0" )
073    public static final String ADDITIONAL_TEXT_LOCATION = "org.tquadrat.foundation.i18n.ap.textLocation";
074
075    /**
076     *  The default name for the resource bundle: {@value}.
077     */
078    @API( status = STABLE, since = "0.1.0" )
079    public static final String DEFAULT_BASEBUNDLENAME = "MessagesAndTexts";
080
081    /**
082     *  The default message prefix: {@value}.
083     */
084    @API( status = STABLE, since = "0.1.0" )
085    public static final String DEFAULT_MESSAGE_PREFIX = "MSG";
086
087        /*--------------*\
088    ====** Constructors **=====================================================
089        \*--------------*/
090    /**
091     *  No instance allowed for this class.
092     */
093    private I18nUtil() { throw new PrivateConstructorForStaticClassCalledError( I18nUtil.class ); }
094
095        /*---------*\
096    ====** Methods **==========================================================
097        \*---------*/
098    /**
099     *  <p>{@summary Composes a message key.}</p>
100     *  <p>The format for the key is like this</p>
101     *  <pre><code>&lt;<i>message_prefix</i>&gt;-&lt;<i>id</i>&gt;</code></pre>
102     *
103     *  @param  messagePrefix   The message prefix.
104     *  @param  id  The message id.
105     *  @return The message key.
106     */
107    @API( status = STABLE, since = "0.1.0" )
108    public static final String composeMessageKey( final String messagePrefix, final String id )
109    {
110        final var retValue = format( "%s-%s", requireNotEmptyArgument( messagePrefix, "messagePrefix" ), requireNotEmptyArgument( id, "id" ) );
111
112        //---* Done *----------------------------------------------------------
113        return retValue;
114    }   //  composeMessageKey()
115
116    /**
117     *  <p>{@summary Composes a message key.}</p>
118     *  <p>The format for the key is like this</p>
119     *  <pre><code>&lt;<i>message_prefix</i>&gt;-&lt;<i>id</i>&gt;</code></pre>
120     *  <p>The id will be a six-digit number, prepended with zeroes if
121     *  required.</p>
122     *
123     *  @param  messagePrefix   The message prefix.
124     *  @param  id  The message id.
125     *  @return The message key.
126     */
127    @API( status = STABLE, since = "0.1.0" )
128    public static final String composeMessageKey( final String messagePrefix, final int id )
129    {
130        final var retValue = format( "%s-%06d", requireNotEmptyArgument( messagePrefix, "messagePrefix" ), id );
131
132        //---* Done *----------------------------------------------------------
133        return retValue;
134    }   //  composeMessageKey()
135
136    /**
137     *  <p>{@summary Composes the resource bundle key for a text.}</p>
138     *  <p>The format for the key is like this:</p>
139     *  <pre><code>&lt;<i>class_name</i>&gt;.&lt;<i>use</i>&gt;_&lt;<i>id</i>&gt;</code></pre>
140     *
141     *  @param  sourceClass The class where the text was defined.
142     *  @param  use The text use.
143     *  @param  id  The id for the text, as from
144     *      {@link org.tquadrat.foundation.i18n.Text#id() &#64;Text.id}.
145     *  @return The text key.
146     */
147    @API( status = STABLE, since = "0.1.0" )
148    public static final String composeTextKey( final Class<?> sourceClass, final TextUse use, final String id )
149    {
150        final var retValue = composeTextKey( requireNonNullArgument( sourceClass, "sourceClass" ).getName(), use, id );
151
152        //---* Done *----------------------------------------------------------
153        return retValue;
154    }   //  composeTextKey()
155
156    /**
157     *  <p>{@summary Composes the resource bundle key for a text.}</p>
158     *  <p>The format for the key is like this:</p>
159     *  <pre><code>&lt;<i>class_name</i>&gt;.&lt;<i>use</i>&gt;_&lt;<i>id</i>&gt;</code></pre>
160     *
161     *  @param  sourceClass The name of the class where the text was defined.
162     *  @param  use The text use.
163     *  @param  id  The id for the text, as from
164     *      {@link org.tquadrat.foundation.i18n.Text#id() &#64;Text.id}.
165     *  @return The text key.
166     */
167    @API( status = STABLE, since = "0.1.0" )
168    public static final String composeTextKey( final String sourceClass, final TextUse use, final String id )
169    {
170        final var retValue = format( "%s.%s_%s", requireNotEmptyArgument( sourceClass, "sourceClass" ), requireNonNullArgument( use, "use" ).name(), requireNotEmptyArgument( id, "name" ) );
171
172        //---* Done *----------------------------------------------------------
173        return retValue;
174    }   //  composeTextKey()
175
176    /**
177     *  <p>{@summary Composes the resource bundle key for an <code>enum</code>
178     *  value.}</p>
179     *  <p>The format for the key is like this:</p>
180     *  <pre><code>&lt;<i>class_name</i>&gt;.STRING_&lt;<i>name</i>&gt;</code></pre>
181     *
182     *  @param  <E> The type of the {@code enum} value.
183     *  @param  value   The {@code enum} value.
184     *  @return The text key.
185     *
186     *  @see Enum#name()
187     */
188    @API( status = STABLE, since = "0.1.0" )
189    public static final <E extends Enum<?>> String composeTextKey( final E value )
190    {
191        final var sourceClass = requireNonNullArgument( value, "value" ).getDeclaringClass();
192        final var id = value.name();
193        final var retValue = composeTextKey( sourceClass, STRING, id );
194
195        //---* Done *----------------------------------------------------------
196        return retValue;
197    }   //  composeTextKey()
198
199    /**
200     *  Creates the fallback text or message when the resource bundle does not
201     *  have a text for the given key.
202     *
203     *  @param  key The failed key.
204     *  @param  args    The arguments.
205     *  @return The fallback text.
206     */
207    @API( status = STABLE, since = "0.1.0" )
208    public static final String createFallback( final String key, final Object... args )
209    {
210        final var retValue = format( "[%s] – %s", requireNotEmptyArgument( key, "key" ), Objects.toString( args ) );
211
212        //---* Done *----------------------------------------------------------
213        return retValue;
214    }   //  createFallback()
215
216    /**
217     *  <p>{@summary Loads the resource bundle with the given base bundle
218     *  name.} If there is no resource bundle for the given base bundle name,
219     *  the return value is
220     *  {@linkplain Optional#empty() empty}.</p>
221     *  <p>If your program is using modules, the module that contains the
222     *  resource bundle must be located in the default package (no package at
223     *  all), or you should use
224     *  {@link #loadResourceBundle(String, Module)}
225     *  instead of this method.</p>
226     *
227     *  @param  baseBundleName  The base bundle name.
228     *  @return An instance of
229     *      {@link Optional}
230     *      that holds the resource bundle.
231     */
232    @SuppressWarnings( "AssignmentToNull" )
233    @API( status = STABLE, since = "0.1.0" )
234    public static final Optional<ResourceBundle> loadResourceBundle( final String baseBundleName )
235    {
236        //---* Force the use of UTF-8 for the resource bundle files *----------
237        setProperty( PROPERTY_RESOURCEBUNDLE_ENCODING, ISO8859_1.name() );
238
239        ResourceBundle bundle;
240        try
241        {
242            final var bundleName = requireNotEmptyArgument( baseBundleName, "baseBundleName" );
243            bundle = ResourceBundle.getBundle( bundleName );
244        }
245        catch( final MissingResourceException e )
246        {
247            ifDebug( e );
248            //noinspection AssignmentToNull
249            bundle = null;
250        }
251
252        final var retValue = Optional.ofNullable( bundle );
253
254        //---* Done *----------------------------------------------------------
255        return retValue;
256    }   //  loadResourceBundle()
257
258    /**
259     *  <p>{@summary Loads the resource bundle with the given base bundle
260     *  name.} If there is no resource bundle for the given base bundle name,
261     *  the return value is
262     *  {@linkplain Optional#empty() empty}.</p>
263     *  <p>Use this method only if your program is using modules; otherwise
264     *  prefer
265     *  {@link #loadResourceBundle(String)}.</p>
266     *  <p>The resource bundle to load must be in an package that is open to
267     *  this module ({@code org.tquadrat.foundation.i18n}) or in no package at
268     *  all.</p>
269     *
270     *  @param  baseBundleName  The base bundle name.
271     *  @param  module  The module that provides the resource bundle; usually,
272     *      this is the caller's module.
273     *  @return An instance of
274     *      {@link Optional}
275     *      that holds the resource bundle.
276     */
277    @SuppressWarnings( "AssignmentToNull" )
278    @API( status = STABLE, since = "0.1.0" )
279    public static final Optional<ResourceBundle> loadResourceBundle( final String baseBundleName, final Module module )
280    {
281        //---* Force the use of UTF-8 for the resource bundle files *----------
282        setProperty( PROPERTY_RESOURCEBUNDLE_ENCODING, ISO8859_1.name() );
283
284        ResourceBundle bundle;
285        try
286        {
287            bundle = ResourceBundle.getBundle( requireNotEmptyArgument( baseBundleName, "baseBundleName" ), requireNonNullArgument( module, "module" ) );
288        }
289        catch( final MissingResourceException e )
290        {
291            ifDebug( e );
292            bundle = null;
293        }
294
295        final var retValue = Optional.ofNullable( bundle );
296
297        //---* Done *----------------------------------------------------------
298        return retValue;
299    }   //  loadResourceBundle()
300
301    /**
302     *  <p>{@summary Returns the Text for the given key, or the alternative
303     *  text.} This method is primarily used internally by the library, but can
304     *  be also useful in other scenarios where the presence of a
305     *  {@link ResourceBundle}
306     *  is not guaranteed.</p>
307     *  <p>If there is a resource bundle,
308     *  {@link #retrieveText(ResourceBundle, String, Object...)}
309     *  should be used instead.</p>
310     *
311     *  @note   <code>text</code> is used as is! The arguments will be applied
312     *      only to a text that will be retrieved from the resource bundle!
313     *
314     *  @param  bundle  The resource bundle.
315     *  @param  text    The alternative text that is used if there is no
316     *      resource bundle, or that bundle does not contain a text for the
317     *      given key.
318     *  @param  textKey The resource bundle key for the text.
319     *  @param  args    The argument for the retrieved text.
320     *  @return The resolved text.
321     */
322    @SuppressWarnings( {"OptionalUsedAsFieldOrParameterType", "BoundedWildcard"} )
323    @API( status = STABLE, since = "0.1.0" )
324    public static final String resolveText( final Optional<ResourceBundle> bundle, final String text, final String textKey, final Object... args )
325    {
326        final var retValue = requireNonNullArgument( bundle, "bundle" ).isPresent() && isNotEmptyOrBlank( textKey )
327            ? retrieveText( bundle.get(), textKey, args )
328            : requireNotEmptyArgument( text, "text" );
329
330        //---* Done *----------------------------------------------------------
331        return retValue;
332    }   //  resolveText()
333
334    /**
335     *  <p>{@summary Returns the Text for the given key, or the alternative
336     *  text.} This method is primarily used internally by the library, but can
337     *  be also useful in other scenarios where the presence of a
338     *  {@link ResourceBundle}
339     *  is not guaranteed.</p>
340     *  <p>If there is a resource bundle,
341     *  {@link #retrieveText(ResourceBundle, String, Object...)}
342     *  should be used instead.</p>
343     *
344     *  @note   <code>text</code> is used as is! The arguments will be applied
345     *      only to a text that will be retrieved from the resource bundle!
346     *
347     *  @param  bundle  The resource bundle.
348     *  @param  text    The alternative text that is used if there is no
349     *      resource bundle, or that bundle does not contain a text for the
350     *      given key.
351     *  @param  textKey The resource bundle key for the text.
352     *  @param  args    The argument for the retrieved text.
353     *  @return The resolved text.
354     */
355    @SuppressWarnings( "OptionalUsedAsFieldOrParameterType" )
356    @API( status = STABLE, since = "0.1.0" )
357    public static final String resolveText( final Optional<ResourceBundle> bundle, final Optional<String> text, final Optional<String> textKey, final Object... args )
358    {
359        final var messageLocal = requireNonNullArgument( text, "text" ).orElse( createFallback( "MissingText", args ) );
360        final var messageKeyLocal = requireNonNullArgument( textKey, "textKey" ).orElse( "MissingTextKey" );
361        final var retValue = resolveText( bundle, messageLocal, messageKeyLocal, args );
362
363        //---* Done *----------------------------------------------------------
364        return retValue;
365    }   //  resolveText()
366
367    /**
368     *  <p>{@summary Returns the Text for the given {@code enum} value.} This
369     *  method is primarily used internally by the library, but can be also
370     *  useful in other scenarios where the presence of a
371     *  {@link ResourceBundle}
372     *  is not guaranteed.</p>
373     *  <p>If there is a resource bundle,
374     *  {@link #retrieveText(ResourceBundle, Enum)}
375     *  should be used instead.</p>
376     *
377     *  @param  <E> The type of the {@code enum} value.
378     *  @param  bundle  The resource bundle.
379     *  @param  value   The {@code enum} value.
380     *  @return The resolved text.
381     */
382    @SuppressWarnings( {"OptionalUsedAsFieldOrParameterType", "BoundedWildcard"} )
383    @API( status = STABLE, since = "0.1.0" )
384    public static final <E extends Enum<?>> String resolveText( final Optional<ResourceBundle> bundle, final E value )
385    {
386        final var retValue = requireNonNullArgument( bundle, "bundle" ).isPresent()
387             ? retrieveText( bundle.get(), value )
388             : requireNotEmptyArgument( value, "value" ).name();
389
390        //---* Done *----------------------------------------------------------
391        return retValue;
392    }   //  resolveText()
393
394    /**
395     *  <p>{@summary Retrieves the message with the given key from the given
396     *  resource bundle and applies the given arguments to it.}</p>
397     *  <p>If the resource bundle does not contain a message for the given key,
398     *  the key itself will be returned, appended with the arguments.</p>
399     *
400     *  @param  bundle  The resource bundle.
401     *  @param  messagePrefix   The message prefix.
402     *  @param  id  The id for the message.
403     *  @param  addKey  The recommended value is {@code true}; this means that
404     *      the message will be prefixed with the generated message key.
405     *  @param  args    The arguments for the message.
406     *  @return The text.
407     */
408    public static final String retrieveMessage( final ResourceBundle bundle, final String messagePrefix, final int id, final boolean addKey, final Object... args )
409    {
410        final var retValue = retrieveMessage( bundle, composeMessageKey( messagePrefix, id ), addKey, args );
411
412        //---* Done *----------------------------------------------------------
413        return retValue;
414    }   //  retrieveMessage()
415
416    /**
417     *  <p>{@summary Retrieves the message with the given key from the given
418     *  resource bundle and applies the given arguments to it.}</p>
419     *  <p>If the resource bundle does not contain a message for the given key,
420     *  the key itself will be returned, appended with the arguments.</p>
421     *
422     *  @param  bundle  The resource bundle.
423     *  @param  messagePrefix   The message prefix.
424     *  @param  id  The id for the message.
425     *  @param  addKey  The recommended value is {@code true}; this means that
426     *      the message will be prefixed with the generated message key.
427     *  @param  args    The arguments for the message.
428     *  @return The text.
429     *
430     *  @see Objects#toString(Object)
431     */
432    public static final String retrieveMessage( final ResourceBundle bundle, final String messagePrefix, final String id, final boolean addKey, final Object... args )
433    {
434        final var retValue = retrieveMessage( bundle, composeMessageKey( messagePrefix, id ), addKey, args );
435
436        //---* Done *----------------------------------------------------------
437        return retValue;
438    }   //  retrieveMessage()
439
440    /**
441     *  The internal implementation for
442     *  {@link #retrieveMessage(ResourceBundle, String, int, boolean, Object...)}
443     *  and
444     *  {@link #retrieveMessage(ResourceBundle, String, String, boolean, Object...)}.
445     *
446     *  @param  bundle  The resource bundle.
447     *  @param  key The key for the message.
448     *  @param  addKey  The recommended value is {@code true}; this means that
449     *      the message will be prefixed with the generated message key.
450     *  @param  args    The arguments for the message.
451     *  @return The text.
452     */
453    @API( status = INTERNAL, since = "0.1.0", consumers = "retrieveMessage()" )
454    private static final String retrieveMessage( final ResourceBundle bundle, final String key, final boolean addKey, final Object... args )
455    {
456        final var message = retrieveText( bundle, key, args );
457        final var retValue = addKey
458             ? format( "[%s] %s", key, message )
459             : message;
460
461        //---* Done *----------------------------------------------------------
462        return retValue;
463    }   //  retrieveMessage()
464
465    /**
466     *  <p>{@summary Retrieves the text with the given key from the given
467     *  resource bundle and applies the given arguments to it.}</p>
468     *  <p>If the resource bundle does not contain a text for the given key,
469     *  the key itself will be returned, appended with the arguments.</p>
470     *
471     *  @param  bundle  The resource bundle.
472     *  @param  key The key for the text.
473     *  @param  args    The arguments for the text.
474     *  @return The text.
475     *
476     *  @see Objects#toString(Object)
477     */
478    public static final String retrieveText( final ResourceBundle bundle, final String key, final Object... args )
479    {
480        requireNonNullArgument( args, "args" );
481        String retValue;
482        try
483        {
484            final var format = requireNonNullArgument( bundle, "bundle" ).getString( requireNotEmptyArgument( key, "key" ) );
485            retValue = format( format, args ).translateEscapes();
486        }
487        catch( final MissingResourceException ignored )
488        {
489            retValue = createFallback( key, args );
490        }
491
492        //---* Done *----------------------------------------------------------
493        return retValue;
494    }   //  retrieveText()
495
496    /**
497     *  <p>{@summary Retrieves the text for the given {@code enum} value from
498     *  the given resource bundle.}</p>
499     *  <p>If the resource bundle does not contain a text for the {@code enum},
500     *  the text key for it will be returned.</p>
501     *
502     *  @param  <E> The type of the {@code enum} value.
503     *  @param  bundle  The resource bundle.
504     *  @param  value   The {@code enum} value.
505     *  @return The text.
506     */
507    public static final <E extends Enum<?>> String retrieveText( final ResourceBundle bundle, final E value )
508    {
509        final var retValue = retrieveText( bundle, composeTextKey( requireNonNullArgument( value, "value" ) ) );
510
511        //---* Done *----------------------------------------------------------
512        return retValue;
513    }   //  retrieveText()
514}
515//  class I18nUtil
516
517/*
518 *  End of File
519 */