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}