001/*
002 * Copyright 2017-2022 Product Mog LLC, 2022-2026 Revetware LLC.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package com.lokalized;
018
019import org.jspecify.annotations.NonNull;
020import org.jspecify.annotations.Nullable;
021
022import java.util.List;
023import java.util.Locale;
024import java.util.Map;
025import java.util.Optional;
026import java.util.stream.Collectors;
027
028import static com.lokalized.Diagnostics.format;
029
030/**
031 * Describes a failed localized string lookup.
032 * <p>
033 * Instances are supplied to a {@link TranslationFailureHandler}. Lokalized constructs these objects; application
034 * code should normally only inspect them.
035 *
036 * @author <a href="https://revetkn.com">Mark Allen</a>
037 * @since 3.0.0
038 */
039public interface TranslationFailure {
040        /**
041         * Gets a concise description of this failure suitable for application logging.
042         * <p>
043         * The message includes the key, lookup locale, failure reason, and attempted locale tags. It does not inspect or
044         * include placeholder values or the runtime cause's message because either may contain sensitive data. The key is
045         * included verbatim and may itself contain placeholder names such as {@code {{name}}}.
046         *
047         * @return the redacted failure message, not null
048         * @since 3.0.0
049         */
050        @NonNull
051        default String getMessage() {
052                return format("Unable to resolve translation key '%s' for locale '%s'. Reason: %s. Attempted locales: [%s]",
053                                getKey(),
054                                getLookupLocale().toLanguageTag(),
055                                getReason(),
056                                getAttemptedLocales().stream()
057                                                .map(Locale::toLanguageTag)
058                                                .collect(Collectors.joining(", ")));
059        }
060
061        /**
062         * Gets the translation key that could not be resolved.
063         *
064         * @return the translation key, not null
065         */
066        @NonNull
067        String getKey();
068
069        /**
070         * Gets the locale used to begin per-key locale fallback after any language-range negotiation.
071         *
072         * @return the lookup locale, not null
073         */
074        @NonNull
075        Locale getLookupLocale();
076
077        /**
078         * Gets strict locale-negotiation diagnostics when the implementation can provide them.
079         *
080         * @return locale-negotiation result, or empty when unavailable, not null
081         */
082        @NonNull
083        default Optional<@NonNull LocaleMatchResult> getLocaleMatchResult() {
084                return Optional.empty();
085        }
086
087        /**
088         * Gets the ordered locales actually attempted for this lookup.
089         *
090         * @return the attempted locales, not null
091         */
092        @NonNull
093        List<@NonNull Locale> getAttemptedLocales();
094
095        /**
096         * Gets the placeholders supplied by the caller.
097         * <p>
098         * The returned map is an unmodifiable shallow copy. Placeholder values may contain sensitive data;
099         * {@link #getMessage()} deliberately excludes them.
100         *
101         * @return the supplied placeholders, not null
102         */
103        @NonNull
104        Map<@NonNull String, @Nullable Object> getPlaceholders();
105
106        /**
107         * Gets the reason the lookup failed.
108         *
109         * @return the reason, not null
110         */
111        @NonNull
112        TranslationFailureReason getReason();
113
114        /**
115         * Gets the runtime cause if an attempted translation existed but could not be resolved. Missing translations and
116         * unmatched alternatives do not have causes.
117         *
118         * @return the cause, not null
119         */
120        @NonNull
121        Optional<@NonNull Throwable> getCause();
122}