All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.lokalized.DefaultStrings Maven / Gradle / Ivy

There is a newer version: 1.0.3
Show newest version
/*
 * Copyright 2017 Product Mog LLC.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.lokalized;

import com.lokalized.LocalizedString.LanguageFormTranslation;
import com.lokalized.LocalizedString.LanguageFormTranslationRange;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.annotation.concurrent.ThreadSafe;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Locale.LanguageRange;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import static java.lang.String.format;
import static java.util.Objects.requireNonNull;

/**
 * Default implementation of a localized string provider.
 * 

* It is recommended to use a single instance of this class across your entire application. *

* In multi-tenant systems like a web application where each user might have a different locale, * your {@code localeSupplier} might return the locale specified by current request. * * @author Mark Allen */ @ThreadSafe public class DefaultStrings implements Strings { @Nonnull private final String fallbackLanguageCode; @Nonnull private final Map> localizedStringsByLocale; @Nullable private final Supplier localeSupplier; @Nullable private final Supplier> languageRangesSupplier; @Nonnull private final FailureMode failureMode; @Nonnull private final Locale fallbackLocale; @Nonnull private final StringInterpolator stringInterpolator; @Nonnull private final ExpressionEvaluator expressionEvaluator; @Nonnull private final Logger logger; /** * Cache of localized strings by key by locale. *

* This is our "master" reference localized string storage that other data structures will point to. */ @Nonnull private final Map> localizedStringsByKeyByLocale; /** * Cache of best-matching strings for the given locale (populated on-demand per request at runtime). *

* List elements are ordered by most to least specific, e.g. if your locale is {@code en-US}, the first list element * might be {@code en-US} strings and the second would be {@code en} strings. *

* There will always be at least one element in the list - the fallback locale. */ @Nonnull private final ConcurrentHashMap> localizedStringSourcesByLocale; /** * Constructs a localized string provider with builder-supplied data. *

* The fallback language code must be an ISO 639 alpha-2 or alpha-3 language code. * When a language has both an alpha-2 code and an alpha-3 code, the alpha-2 code must be used. * * @param fallbackLanguageCode fallback language code, not null * @param localizedStringSupplier supplier of localized strings, not null * @param localeSupplier locale supplier, may be null * @param languageRangesSupplier language ranges supplier, may be null * @param failureMode strategy for dealing with lookup failures, may be null */ protected DefaultStrings(@Nonnull String fallbackLanguageCode, @Nonnull Supplier>> localizedStringSupplier, @Nullable Supplier localeSupplier, @Nullable Supplier> languageRangesSupplier, @Nullable FailureMode failureMode) { requireNonNull(fallbackLanguageCode); requireNonNull(localizedStringSupplier); this.logger = Logger.getLogger(LoggerType.STRINGS.getLoggerName()); Map> suppliedLocalizedStringsByLocale = localizedStringSupplier.get(); if (suppliedLocalizedStringsByLocale == null) suppliedLocalizedStringsByLocale = Collections.emptyMap(); // Defensive copy of iterator to unmodifiable set Map> localizedStringsByLocale = suppliedLocalizedStringsByLocale.entrySet().stream() .collect(Collectors.toMap( entry -> entry.getKey(), entry -> { Set localizedStrings = new LinkedHashSet<>(); entry.getValue().forEach(localizedStrings::add); return Collections.unmodifiableSet(localizedStrings); } )); this.fallbackLocale = Locale.forLanguageTag(fallbackLanguageCode); this.fallbackLanguageCode = fallbackLanguageCode; this.localizedStringsByLocale = Collections.unmodifiableMap(localizedStringsByLocale); this.languageRangesSupplier = languageRangesSupplier; this.failureMode = failureMode == null ? FailureMode.USE_FALLBACK : failureMode; this.stringInterpolator = new StringInterpolator(); this.expressionEvaluator = new ExpressionEvaluator(); this.localizedStringsByKeyByLocale = Collections.unmodifiableMap(localizedStringsByLocale.entrySet().stream() .collect(Collectors.toMap( entry1 -> entry1.getKey(), entry1 -> Collections.unmodifiableMap(entry1.getValue().stream() .collect(Collectors.toMap( entry2 -> entry2.getKey(), entry2 -> entry2 ) ))))); this.localizedStringSourcesByLocale = new ConcurrentHashMap<>(); if (!localizedStringsByLocale.containsKey(getFallbackLocale())) throw new IllegalArgumentException(format("Specified fallback language code is '%s' but no matching " + "localized strings locale was found. Known locales: [%s]", fallbackLanguageCode, localizedStringsByLocale.keySet().stream() .map(locale -> locale.toLanguageTag()) .sorted() .collect(Collectors.joining(", ")))); if (localeSupplier != null && languageRangesSupplier != null) throw new IllegalArgumentException(format("You cannot provide both a localeSupplier " + "and a languageRangesSupplier when building an instance of %s - you must pick one of the two.", getClass().getSimpleName())); if (localeSupplier == null && languageRangesSupplier == null) this.localeSupplier = () -> getFallbackLocale(); else this.localeSupplier = localeSupplier; } @Nonnull @Override public String get(@Nonnull String key) { requireNonNull(key); return get(key, null, null); } @Nonnull @Override public String get(@Nonnull String key, @Nullable Locale locale) { requireNonNull(key); return get(key, null, locale); } @Nonnull @Override public String get(@Nonnull String key, @Nullable Map placeholders) { requireNonNull(key); return get(key, placeholders, null); } @Nonnull @Override public String get(@Nonnull String key, @Nullable Map placeholders, @Nullable Locale locale) { requireNonNull(key); if (placeholders == null) placeholders = Collections.emptyMap(); if (locale == null) locale = getImplicitLocale(); String translation = null; Map mutableContext = new HashMap<>(placeholders); Map immutableContext = Collections.unmodifiableMap(placeholders); List localizedStringSources = getLocalizedStringSourcesForLocale(locale); for (LocalizedStringSource localizedStringSource : localizedStringSources) { LocalizedString localizedString = localizedStringSource.getLocalizedStringsByKey().get(key); if (localizedString == null) { logger.finer(format("No match for '%s' was found in '%s'", key, localizedStringSource.getLocale().toLanguageTag())); } else { logger.finer(format("A match for '%s' was found in '%s'", key, localizedStringSource.getLocale().toLanguageTag())); translation = getInternal(key, localizedString, mutableContext, immutableContext, localizedStringSource.getLocale()).orElse(null); break; } } if (translation == null) { logger.finer(format("No match for '%s' was found in any strings file.", key)); translation = key; } return translation; } /** * Recursive method which attempts to translate a localized string. * * @param key the toplevel translation key (always the same regardless of recursion depth), not null * @param localizedString the localized string on which to operate, not null * @param mutableContext the mutable context for the translation, not null * @param immutableContext the original user-supplied translation context, not null * @param locale the locale to use for evaluation, not null * @return the translation, if possible (may not be possible if no translation value specified and no alternative expressions match), not null */ @Nonnull protected Optional getInternal(@Nonnull String key, @Nonnull LocalizedString localizedString, @Nonnull Map mutableContext, @Nonnull Map immutableContext, @Nonnull Locale locale) { requireNonNull(key); requireNonNull(localizedString); requireNonNull(mutableContext); requireNonNull(immutableContext); requireNonNull(locale); // First, see if any alternatives match by evaluating them for (LocalizedString alternative : localizedString.getAlternatives()) { if (getExpressionEvaluator().evaluate(alternative.getKey(), mutableContext, locale)) { logger.finer(format("An alternative match for '%s' was found for key '%s' and context %s", alternative.getKey(), key, mutableContext)); // If we have a matching alternative, recurse into it return getInternal(key, alternative, mutableContext, immutableContext, locale); } } if (!localizedString.getTranslation().isPresent()) return Optional.empty(); String translation = localizedString.getTranslation().get(); for (Entry entry : localizedString.getLanguageFormTranslationsByPlaceholder().entrySet()) { String placeholderName = entry.getKey(); LanguageFormTranslation languageFormTranslation = entry.getValue(); Object value = null; Object rangeStart = null; Object rangeEnd = null; Map translationsByCardinality = new HashMap<>(); Map translationsByOrdinality = new HashMap<>(); Map translationsByGender = new HashMap<>(); if (languageFormTranslation.getRange().isPresent()) { LanguageFormTranslationRange languageFormTranslationRange = languageFormTranslation.getRange().get(); rangeStart = immutableContext.get(languageFormTranslationRange.getStart()); rangeEnd = immutableContext.get(languageFormTranslationRange.getEnd()); } else { value = immutableContext.get(languageFormTranslation.getValue().get()); } for (Entry translationEntry : languageFormTranslation.getTranslationsByLanguageForm().entrySet()) { LanguageForm languageForm = translationEntry.getKey(); String translatedLanguageForm = translationEntry.getValue(); if (languageForm instanceof Cardinality) translationsByCardinality.put((Cardinality) languageForm, translatedLanguageForm); else if (languageForm instanceof Ordinality) translationsByOrdinality.put((Ordinality) languageForm, translatedLanguageForm); else if (languageForm instanceof Gender) translationsByGender.put((Gender) languageForm, translatedLanguageForm); else throw new IllegalArgumentException(format("Encountered unrecognized language form %s", languageForm)); } int distinctLanguageForms = (translationsByCardinality.size() > 0 ? 1 : 0) + (translationsByOrdinality.size() > 0 ? 1 : 0) + (translationsByGender.size() > 0 ? 1 : 0); if (distinctLanguageForms > 1) throw new IllegalArgumentException(format("You cannot mix-and-match language forms. Offending localized string was %s", localizedString)); if (distinctLanguageForms == 0) continue; // Handle plural cardinalities if (translationsByCardinality.size() > 0) { // Special case: calculate range from min and max if this is a range-driven cardinality if (languageFormTranslation.getRange().isPresent()) { if (rangeStart == null) rangeStart = 0; if (rangeEnd == null) rangeEnd = 0; if (!(rangeStart instanceof Number)) { logger.warning(format("Range start '%s' for '%s' is not a number, falling back to 0.", rangeStart, languageFormTranslation.getValue())); rangeStart = 0; } if (!(rangeEnd instanceof Number)) { logger.warning(format("Range value end '%s' for '%s' is not a number, falling back to 0.", rangeEnd, languageFormTranslation.getValue())); rangeEnd = 0; } Cardinality startCardinality = Cardinality.forNumber((Number) rangeStart, locale); Cardinality endCardinality = Cardinality.forNumber((Number) rangeEnd, locale); Cardinality rangeCardinality = Cardinality.forRange(startCardinality, endCardinality, locale); String cardinalityTranslation = translationsByCardinality.get(rangeCardinality); if (cardinalityTranslation == null) logger.warning(format("Unable to find %s translation for range cardinality %s (start was %s, end was %s). Localized string was %s", Cardinality.class.getSimpleName(), rangeCardinality.name(), startCardinality.name(), endCardinality.name(), localizedString)); mutableContext.put(placeholderName, cardinalityTranslation); } else { // Normal "non-range" cardinality if (value == null) value = 0; if (!(value instanceof Number)) { logger.warning(format("Value '%s' for '%s' is not a number, falling back to 0.", value, languageFormTranslation.getValue())); value = 0; } Cardinality cardinality = Cardinality.forNumber((Number) value, locale); String cardinalityTranslation = translationsByCardinality.get(cardinality); if (cardinalityTranslation == null) logger.warning(format("Unable to find %s translation for %s. Localized string was %s", Cardinality.class.getSimpleName(), cardinality.name(), localizedString)); mutableContext.put(placeholderName, cardinalityTranslation); } } // Handle plural ordinalities if (translationsByOrdinality.size() > 0) { if (value == null) value = 0; if (!(value instanceof Number)) { logger.warning(format("Value '%s' for '%s' is not a number, falling back to 0.", value, languageFormTranslation.getValue())); value = 0; } Ordinality ordinality = Ordinality.forNumber((Number) value, locale); String ordinalityTranslation = translationsByOrdinality.get(ordinality); if (ordinalityTranslation == null) logger.warning(format("Unable to find %s translation for %s. Localized string was %s", Ordinality.class.getSimpleName(), ordinality.name(), localizedString)); mutableContext.put(placeholderName, ordinalityTranslation); } // Handle genders if (translationsByGender.size() > 0) { if (value == null) { logger.warning(format("Value '%s' for '%s' is null. No replacement will be performed.", value, languageFormTranslation.getValue())); continue; } if (!(value instanceof Gender)) { logger.warning(format("Value '%s' for '%s' is not a %s. No replacement will be performed.", value, languageFormTranslation.getValue(), Gender.class.getSimpleName())); continue; } Gender gender = (Gender) value; String genderTranslation = translationsByGender.get(gender); if (genderTranslation == null) logger.warning(format("Unable to find %s translation for %s. Localized string was %s", Gender.class.getSimpleName(), gender.name(), localizedString)); mutableContext.put(placeholderName, genderTranslation); } } translation = stringInterpolator.interpolate(translation, mutableContext); return Optional.of(translation); } @Nonnull protected List getLocalizedStringSourcesForLocale(@Nonnull Locale locale) { requireNonNull(locale); return getLocalizedStringSourcesByLocale().computeIfAbsent(locale, (ignored) -> { String language = LocaleUtils.normalizedLanguage(locale).orElse(null); String script = locale.getScript(); String country = locale.getCountry(); String variant = locale.getVariant(); Set extensionKeys = locale.hasExtensions() ? locale.getExtensionKeys() : Collections.emptySet(); Set localizedStrings; Set matchingLocales = new HashSet<>(5); List localizedStringSources = new ArrayList<>(5); if (logger.isLoggable(Level.FINER)) logger.finer(format("Finding strings files that match locale '%s'...", locale.toLanguageTag())); // Try most specific (matches all 5 criteria) and move back to least specific Locale.Builder extensionsLocaleBuilder = new Locale.Builder().setLanguage(language).setScript(script).setRegion(country).setVariant(variant); for (Character extensionKey : extensionKeys) extensionsLocaleBuilder.setExtension(extensionKey, locale.getExtension(extensionKey)); Locale extensionsLocale = extensionsLocaleBuilder.build(); matchingLocales.add(extensionsLocale); localizedStrings = getLocalizedStringsByLocale().get(extensionsLocale); if (localizedStrings != null) { localizedStringSources.add(new LocalizedStringSource(extensionsLocale, getLocalizedStringsByKeyByLocale().get(extensionsLocale))); if (logger.isLoggable(Level.FINER)) logger.finer(format("A matching strings file for locale '%s' is '%s'", locale.toLanguageTag(), extensionsLocale.toLanguageTag())); } // Variant (4) Locale variantLocale = new Locale.Builder().setLanguage(language).setScript(script).setRegion(country).setVariant(variant) .build(); if (!matchingLocales.contains(variantLocale)) { matchingLocales.add(variantLocale); localizedStrings = getLocalizedStringsByLocale().get(variantLocale); if (localizedStrings != null) { localizedStringSources.add(new LocalizedStringSource(variantLocale, getLocalizedStringsByKeyByLocale().get(variantLocale))); if (logger.isLoggable(Level.FINER)) logger.finer(format("A matching strings file for locale '%s' is '%s'", locale.toLanguageTag(), variantLocale.toLanguageTag())); } } // Region (3) Locale regionLocale = new Locale.Builder().setLanguage(language).setScript(script).setRegion(country).build(); if (!matchingLocales.contains(regionLocale)) { matchingLocales.add(regionLocale); localizedStrings = getLocalizedStringsByLocale().get(regionLocale); if (localizedStrings != null) { localizedStringSources.add(new LocalizedStringSource(regionLocale, getLocalizedStringsByKeyByLocale().get(regionLocale))); if (logger.isLoggable(Level.FINER)) logger.finer(format("A matching strings file for locale '%s' is '%s'", locale.toLanguageTag(), regionLocale.toLanguageTag())); } } // Script (2) Locale scriptLocale = new Locale.Builder().setLanguage(language).setScript(script).build(); if (!matchingLocales.contains(scriptLocale)) { matchingLocales.add(scriptLocale); localizedStrings = getLocalizedStringsByLocale().get(scriptLocale); if (localizedStrings != null) { localizedStringSources.add(new LocalizedStringSource(scriptLocale, getLocalizedStringsByKeyByLocale().get(scriptLocale))); if (logger.isLoggable(Level.FINER)) logger.finer(format("A matching strings file for locale '%s' is '%s'", locale.toLanguageTag(), scriptLocale.toLanguageTag())); } } // Language (1) Locale languageLocale = new Locale.Builder().setLanguage(language).build(); if (!matchingLocales.contains(languageLocale)) { matchingLocales.add(languageLocale); localizedStrings = getLocalizedStringsByLocale().get(languageLocale); if (localizedStrings != null) { localizedStringSources.add(new LocalizedStringSource(languageLocale, getLocalizedStringsByKeyByLocale().get(languageLocale))); if (logger.isLoggable(Level.FINER)) logger.finer(format("A matching strings file for locale '%s' is '%s'", locale.toLanguageTag(), languageLocale.toLanguageTag())); } } // Finally, add the default locale if necessary Locale fallbackLocale = getFallbackLocale(); if (!matchingLocales.contains(fallbackLocale)) { matchingLocales.add(fallbackLocale); localizedStrings = getLocalizedStringsByLocale().get(fallbackLocale); if (localizedStrings != null) { localizedStringSources.add(new LocalizedStringSource(fallbackLocale, getLocalizedStringsByKeyByLocale().get(fallbackLocale))); if (logger.isLoggable(Level.FINER)) logger.finer(format("A matching strings file for locale '%s' is fallback '%s'", locale.toLanguageTag(), fallbackLocale.toLanguageTag())); } } return Collections.unmodifiableList(localizedStringSources); }); } /** * Gets the fallback language code. * * @return the fallback language code, not null */ @Nonnull public String getFallbackLanguageCode() { return fallbackLanguageCode; } /** * Gets the set of localized strings for each locale. * * @return the set of localized strings for each locale, not null */ @Nonnull public Map> getLocalizedStringsByLocale() { return localizedStringsByLocale; } /** * Gets the locale supplier. * * @return the locale supplier, not null */ @Nonnull protected Optional> getLocaleSupplier() { return Optional.ofNullable(localeSupplier); } /** * Gets the language ranges supplier. * * @return the language ranges supplier, not null */ @Nonnull protected Optional>> getLanguageRangesSupplier() { return Optional.ofNullable(languageRangesSupplier); } /** * Gets the strategy for handling string lookup failures. * * @return the strategy for handling string lookup failures, not null */ @Nonnull public FailureMode getFailureMode() { return failureMode; } /** * Gets the fallback locale. * * @return the fallback locale, not null */ @Nonnull protected Locale getFallbackLocale() { return fallbackLocale; } /** * Gets the locale to use if one was not explicitly provided. * * @return the implicit locale to use, not null */ @Nonnull protected Locale getImplicitLocale() { Locale locale = null; if (getLocaleSupplier().isPresent()) { locale = getLocaleSupplier().get().get(); } else if (getLanguageRangesSupplier().isPresent()) { List languageRanges = getLanguageRangesSupplier().get().get(); if (languageRanges != null) locale = Locale.lookup(languageRanges, getLocalizedStringsByLocale().keySet()); } return locale == null ? getFallbackLocale() : locale; } /** * Gets the string interpolator used to merge placeholders into translations. * * @return the string interpolator, not null */ @Nonnull protected StringInterpolator getStringInterpolator() { return stringInterpolator; } /** * Gets the expression evaluator used to determine if alternative expressions match the evaluation context. * * @return the expression evaluator, not null */ @Nonnull protected ExpressionEvaluator getExpressionEvaluator() { return expressionEvaluator; } /** * Gets our "master" cache of localized strings by key by locale. * * @return the cache of localized strings by key by locale, not null */ @Nonnull protected Map> getLocalizedStringsByKeyByLocale() { return localizedStringsByKeyByLocale; } /** * Get the "runtime" generated map of locales to localized string sources. * * @return the map of locales to localized string sources, not null */ @Nonnull protected ConcurrentHashMap> getLocalizedStringSourcesByLocale() { return localizedStringSourcesByLocale; } /** * Data structure which holds a locale and the localized strings for it, with the strings mapped by key for fast access. * * @author Mark Allen */ @Immutable static class LocalizedStringSource { @Nonnull private final Locale locale; @Nonnull private final Map localizedStringsByKey; /** * Constructs a localized string source with the given locale and map of keys to localized strings. * * @param locale the locale for these localized strings, not null * @param localizedStringsByKey localized strings by translation key, not null */ public LocalizedStringSource(@Nonnull Locale locale, @Nonnull Map localizedStringsByKey) { requireNonNull(locale); requireNonNull(localizedStringsByKey); this.locale = locale; this.localizedStringsByKey = localizedStringsByKey; } /** * Generates a {@code String} representation of this object. * * @return a string representation of this object, not null */ @Override @Nonnull public String toString() { return format("%s{locale=%s, localizedStringsByKey=%s", getClass().getSimpleName(), getLocale(), getLocalizedStringsByKey()); } /** * Checks if this object is equal to another one. * * @param other the object to check, null returns false * @return true if this is equal to the other object, false otherwise */ @Override public boolean equals(@Nullable Object other) { if (this == other) return true; if (other == null || !getClass().equals(other.getClass())) return false; LocalizedStringSource localizedStringSource = (LocalizedStringSource) other; return Objects.equals(getLocale(), localizedStringSource.getLocale()) && Objects.equals(getLocalizedStringsByKey(), localizedStringSource.getLocalizedStringsByKey()); } /** * A hash code for this object. * * @return a suitable hash code */ @Override public int hashCode() { return Objects.hash(getLocale(), getLocalizedStringsByKey()); } @Nonnull public Locale getLocale() { return locale; } @Nonnull public Map getLocalizedStringsByKey() { return localizedStringsByKey; } } /** * Strategies for handling localized string lookup failures. */ public enum FailureMode { /** * The system will attempt a series of fallbacks in order to not throw an exception at runtime. *

* This mode is useful for production, where we often want program execution to continue in the face of * localization errors. */ USE_FALLBACK, /** * The system will throw an exception if a localization is missing for the specified locale. *

* This mode is useful for testing, since problems are uncovered right away when execution halts. */ FAIL_FAST } /** * Builder used to construct instances of {@link DefaultStrings}. *

* You cannot provide both a {@code localeSupplier} and a {@code languageRangesSupplier} - you must choose one or neither. *

* This class is intended for use by a single thread. * * @author Mark Allen */ @NotThreadSafe public static class Builder { @Nonnull private final String fallbackLanguageCode; @Nonnull private final Supplier>> localizedStringSupplier; @Nullable private Supplier localeSupplier; @Nullable private Supplier> languageRangesSupplier; @Nullable private FailureMode failureMode; /** * Constructs a strings builder with a default language code and localized string supplier. *

* The fallback language code must be an ISO 639 alpha-2 or alpha-3 language code. * When a language has both an alpha-2 code and an alpha-3 code, the alpha-2 code must be used. * * @param fallbackLanguageCode fallback language code, not null * @param localizedStringSupplier supplier of localized strings, not null */ public Builder(@Nonnull String fallbackLanguageCode, @Nonnull Supplier>> localizedStringSupplier) { requireNonNull(fallbackLanguageCode); requireNonNull(localizedStringSupplier); this.fallbackLanguageCode = fallbackLanguageCode; this.localizedStringSupplier = localizedStringSupplier; } /** * Applies a locale supplier to this builder. * * @param localeSupplier locale supplier, may be null * @return this builder instance, useful for chaining. not null */ @Nonnull public Builder localeSupplier(@Nullable Supplier localeSupplier) { this.localeSupplier = localeSupplier; return this; } /** * Applies a supplier of language ranges to this builder. * * @param languageRangesSupplier language ranges supplier, may be null * @return this builder instance, useful for chaining. not null */ @Nonnull public Builder languageRangesSupplier(@Nullable Supplier> languageRangesSupplier) { this.languageRangesSupplier = languageRangesSupplier; return this; } /** * Applies a failure mode to this builder. * * @param failureMode strategy for dealing with lookup failures, may be null * @return this builder instance, useful for chaining. not null */ @Nonnull public Builder failureMode(@Nullable FailureMode failureMode) { this.failureMode = failureMode; return this; } /** * Constructs an instance of {@link DefaultStrings}. * * @return an instance of {@link DefaultStrings}, not null */ @Nonnull public DefaultStrings build() { return new DefaultStrings(fallbackLanguageCode, localizedStringSupplier, localeSupplier, languageRangesSupplier, failureMode); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy