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><<i>message_prefix</i>>-<<i>id</i>></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><<i>message_prefix</i>>-<<i>id</i>></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><<i>class_name</i>>.<<i>use</i>>_<<i>id</i>></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() @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><<i>class_name</i>>.<<i>use</i>>_<<i>id</i>></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() @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><<i>class_name</i>>.STRING_<<i>name</i>></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 */