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}