Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.lokalized.DefaultStrings Maven / Gradle / Ivy
Go to download
Lokalized facilitates natural-sounding software translations.
/*
* 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);
}
}
}