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 javax.annotation.concurrent.ThreadSafe;
023import java.util.Locale;
024
025import static java.util.Objects.requireNonNull;
026
027/**
028 * Decides whether a failed locale attempt should continue to the next locale candidate.
029 * <p>
030 * This policy is intentionally separate from {@link TranslationFailureHandler}: fallback policy controls which
031 * locale candidates are attempted, while the failure handler controls the final response after fallback stops or all candidates
032 * are exhausted.
033 * <p>
034 * A failure while evaluating a reachable {@link LocalizedString.ExpressionAlternative expression-fragment predicate}
035 * or interpolating its selected/default fragment is a {@link TranslationFailureReason#RESOLUTION_FAILURE}. The safe
036 * default {@link #fallbackOnMissingTranslationOrNoMatchingAlternative()} stops at that failure;
037 * {@link #fallbackOnAnyFailure()} may continue to another locale candidate. Expression-selected generated fragments
038 * always have a default translation, so their selection cannot itself produce
039 * {@link TranslationFailureReason#NO_MATCHING_ALTERNATIVE}.
040 * <p>
041 * Implementations may be invoked concurrently and must be thread-safe when shared by a {@link Strings} instance.
042 *
043 * @author <a href="https://revetkn.com">Mark Allen</a>
044 * @since 3.0.0
045 */
046@ThreadSafe
047@FunctionalInterface
048public interface TranslationFallbackPolicy {
049        /**
050         * Determines whether resolution should continue with the next locale candidate.
051         *
052         * @param reason          reason the current locale attempt failed, not null
053         * @param attemptedLocale locale whose localized strings were attempted, not null
054         * @param cause           runtime cause for {@link TranslationFailureReason#RESOLUTION_FAILURE}, otherwise null
055         * @return whether to try the next candidate, not null
056         */
057        @NonNull
058        Boolean shouldTryNextLocale(@NonNull TranslationFailureReason reason, @NonNull Locale attemptedLocale,
059                                                                                                                        @Nullable Throwable cause);
060
061        /**
062         * Falls back for missing translations and unmatched whole-message alternatives, but stops on resolution failures.
063         * <p>
064         * This is the recommended safe default. It stops, for example, when an evaluated expression-fragment predicate is
065         * invalid or its selected/default fragment cannot be interpolated.
066         *
067         * @return the recommended safe default policy, not null
068         */
069        @NonNull
070        static TranslationFallbackPolicy fallbackOnMissingTranslationOrNoMatchingAlternative() {
071                return DefaultTranslationFallbackPolicy.MISSING_OR_NO_MATCH;
072        }
073
074        /**
075         * Falls back after every failed locale attempt, including runtime resolution failures.
076         * <p>
077         * This may continue to another locale after an evaluated expression-fragment predicate or selected/default fragment
078         * fails. If no later locale resolves, the configured {@link TranslationFailureHandler} receives the resolution
079         * failure.
080         * <p>
081         * This preserves Lokalized 2.x behavior.
082         *
083         * @return the permissive fallback policy, not null
084         */
085        @NonNull
086        static TranslationFallbackPolicy fallbackOnAnyFailure() {
087                return DefaultTranslationFallbackPolicy.ANY_FAILURE;
088        }
089
090        /**
091         * Never falls back after a locale attempt fails.
092         *
093         * @return the fail-fast fallback policy, not null
094         */
095        @NonNull
096        static TranslationFallbackPolicy neverFallback() {
097                return DefaultTranslationFallbackPolicy.NEVER;
098        }
099}
100
101enum DefaultTranslationFallbackPolicy implements TranslationFallbackPolicy {
102        MISSING_OR_NO_MATCH {
103                @Override
104                public @NonNull Boolean shouldTryNextLocale(@NonNull TranslationFailureReason reason,
105                                @NonNull Locale attemptedLocale, @Nullable Throwable cause) {
106                        requireNonNull(reason);
107                        requireNonNull(attemptedLocale);
108                        return reason != TranslationFailureReason.RESOLUTION_FAILURE;
109                }
110        },
111        ANY_FAILURE {
112                @Override
113                public @NonNull Boolean shouldTryNextLocale(@NonNull TranslationFailureReason reason,
114                                @NonNull Locale attemptedLocale, @Nullable Throwable cause) {
115                        requireNonNull(reason);
116                        requireNonNull(attemptedLocale);
117                        return true;
118                }
119        },
120        NEVER {
121                @Override
122                public @NonNull Boolean shouldTryNextLocale(@NonNull TranslationFailureReason reason,
123                                @NonNull Locale attemptedLocale, @Nullable Throwable cause) {
124                        requireNonNull(reason);
125                        requireNonNull(attemptedLocale);
126                        return false;
127                }
128        }
129}