mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
refactor(i18n): ask the catalogue for a sentence in one helper
`t()` answers a missing key with the key, so "does this code have a sentence" was a comparison written inline at every site that wanted a code's sentence or a fallback, twenty-odd times in several spellings. translateOrNull, translateCodeOrNull and translateCode in src/lib/i18n/translate-code.ts now hold that probe, every site uses them, and a guard refuses the inline comparison anywhere else.
This commit is contained in:
1 parent
2f7f04ecab
commit
6e54f33901
32 files changed
+291
-119
No files matched your search
@@ -21,6 +21,7 @@ import type { CoachAssumption, CoachFollowUp } from "@/lib/ai/coach/types";
|
||||
|
||||
import { SuggestedReplies } from "./suggested-replies";
|
||||
import { COACH_FOCUS_RING } from "./focus-ring";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
|
||||
/** The alternatives the server offers for one assumption. */
|
||||
export function alternativesFor(
|
||||
@@ -39,8 +40,7 @@ function optionLabel(
|
||||
t: (key: string) => string,
|
||||
option: CoachAssumption["value"],
|
||||
): string {
|
||||
const local = t(option.labelKey);
|
||||
return local === option.labelKey ? option.label : local;
|
||||
return translateOrNull(t, option.labelKey) ?? option.label;
|
||||
}
|
||||
|
||||
export interface CoachAssumptionLineProps {
|
||||
|
||||
@@ -38,6 +38,7 @@ import type {
|
||||
CoachMemoryNote as CoachMemoryNoteFrame,
|
||||
CoachProvenanceMetric,
|
||||
} from "@/lib/ai/coach/types";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
|
||||
/**
|
||||
* Per-message bubble renderer for the Coach thread, split out of
|
||||
@@ -239,8 +240,8 @@ function coachChartTitle(
|
||||
): string {
|
||||
const override = CHART_TITLE_KEY_OVERRIDE[metric];
|
||||
if (override) {
|
||||
const resolved = t(override);
|
||||
if (resolved !== override) return resolved;
|
||||
const resolved = translateOrNull(t, override);
|
||||
if (resolved !== null) return resolved;
|
||||
}
|
||||
const camel = metric
|
||||
.toLowerCase()
|
||||
@@ -250,8 +251,7 @@ function coachChartTitle(
|
||||
)
|
||||
.join("");
|
||||
const key = `measurements.type${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
||||
const resolved = t(key);
|
||||
return resolved === key ? metric.replace(/_/g, " ").toLowerCase() : resolved;
|
||||
return translateOrNull(t, key) ?? metric.replace(/_/g, " ").toLowerCase();
|
||||
}
|
||||
|
||||
interface ChatBubbleProps {
|
||||
@@ -564,16 +564,11 @@ function ChatBubbleImpl({
|
||||
// provider load (retry in ~5 min). Codes that have no dedicated
|
||||
// translation fall back to the generic provider-unavailable copy.
|
||||
const errorKey = errorCode ? errorCodeToI18nKey(errorCode) : null;
|
||||
const errorMessage = errorKey ? t(errorKey, {}) : null;
|
||||
// When a translated message comes back unchanged (i.e. key missing)
|
||||
// we fall back to a generic provider error string so the bubble
|
||||
// doesn't surface raw `coach.http.503` text to the user.
|
||||
// A code without a sentence falls back to the generic provider error, so
|
||||
// the bubble never surfaces raw `coach.http.503` text to the user.
|
||||
const safeError =
|
||||
errorMessage && errorMessage !== errorKey
|
||||
? errorMessage
|
||||
: errorCode
|
||||
? t("insights.coach.errorProvider")
|
||||
: null;
|
||||
(errorKey ? translateOrNull(t, errorKey) : null) ??
|
||||
(errorCode ? t("insights.coach.errorProvider") : null);
|
||||
|
||||
const method = metricSource?.method ?? null;
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ import {
|
||||
useDetachCoachDocument,
|
||||
useSendCoachMessage,
|
||||
} from "./use-coach";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
|
||||
/**
|
||||
* v1.12.0 (Coach v2 #6) — shared chat surface for both the Coach drawer
|
||||
@@ -987,11 +988,8 @@ export function CoachConversation({
|
||||
// from a metric surface names the metric there until its first turn.
|
||||
const a2Metric = launchScope?.metric ?? null;
|
||||
|
||||
// `t()` returns the key string itself when a key is missing, so the only
|
||||
// reliable "is this key defined" signal is `t(key) !== key`.
|
||||
function tOrNull(key: string): string | null {
|
||||
const resolved = t(key);
|
||||
return resolved === key ? null : resolved;
|
||||
return translateOrNull(t, key);
|
||||
}
|
||||
|
||||
// The resolved metric label: prefers a per-source i18n key, falls back to
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
COACH_MEMORY_KEYS,
|
||||
COACH_PLAN_KEYS,
|
||||
} from "@/lib/ai/coach/memory/shared";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
|
||||
// v1.28.26 file-size split (pure code motion): the bubble renderer +
|
||||
// per-message actions live in `chat-bubble.tsx`, the read-aloud stack in
|
||||
@@ -215,8 +216,7 @@ export function decisionReplies(
|
||||
]
|
||||
: choices;
|
||||
for (const choice of ordered.slice(0, 4)) {
|
||||
const local = t(choice.labelKey);
|
||||
const label = local === choice.labelKey ? choice.label : local;
|
||||
const label = translateOrNull(t, choice.labelKey) ?? choice.label;
|
||||
out.push({
|
||||
id: `clarify-${choice.id}`,
|
||||
label,
|
||||
|
||||
@@ -82,6 +82,7 @@ import type {
|
||||
|
||||
import { COACH_FOCUS_RING } from "./focus-ring";
|
||||
import { CoachMethodLine } from "./method-line";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
|
||||
/** The shortest gap between two screen-reader announcements. */
|
||||
export const ANNOUNCE_THROTTLE_MS = 1500;
|
||||
@@ -106,8 +107,7 @@ function tOr(
|
||||
fallback: string,
|
||||
params?: Record<string, string | number>,
|
||||
) {
|
||||
const value = t(key, params);
|
||||
return value === key ? fallback : value;
|
||||
return translateOrNull(t, key, params) ?? fallback;
|
||||
}
|
||||
|
||||
// ── Steps (messages saved before v1.41, and the fetch rows) ────────────────
|
||||
@@ -180,9 +180,8 @@ export function legacyAreaLabels(
|
||||
const out: string[] = [];
|
||||
for (const area of areas ?? []) {
|
||||
if (area === "general") continue;
|
||||
const key = `insights.coach.metric.${area}`;
|
||||
const label = t(key);
|
||||
if (label === key || out.includes(label)) continue;
|
||||
const label = translateOrNull(t, `insights.coach.metric.${area}`);
|
||||
if (label === null || out.includes(label)) continue;
|
||||
out.push(label);
|
||||
}
|
||||
return out;
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { WorkoutDetailPayload } from "@/hooks/use-workouts";
|
||||
import { useUnitDisplay } from "@/hooks/use-unit-display";
|
||||
|
||||
import { formatDuration, formatDistance } from "./format";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
|
||||
/**
|
||||
* Hero header — the at-a-glance verdict row. Sport icon badge, source
|
||||
@@ -34,9 +35,7 @@ export function WorkoutDetailHeader({ workout }: WorkoutDetailHeaderProps) {
|
||||
const fmt = useFormatters();
|
||||
const { preference } = useUnitDisplay();
|
||||
const sportLabelKey = `insights.workouts.sport.${workout.sportType}`;
|
||||
const sportLabel = t(sportLabelKey);
|
||||
const sportName =
|
||||
sportLabel === sportLabelKey ? workout.sportType : sportLabel;
|
||||
const sportName = translateOrNull(t, sportLabelKey) ?? workout.sportType;
|
||||
|
||||
return (
|
||||
<header
|
||||
|
||||
@@ -13,6 +13,7 @@ import type { WorkoutListEntry } from "@/hooks/use-workouts";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useUnitDisplay } from "@/hooks/use-unit-display";
|
||||
import { formatDistance } from "@/components/insights/workout-detail/format";
|
||||
import { translateOrNull } from "@/lib/i18n/translate-code";
|
||||
|
||||
/**
|
||||
* v1.4.32 — workout list row primitive.
|
||||
@@ -63,12 +64,11 @@ export function WorkoutList({
|
||||
{workouts.map((workout) => {
|
||||
const Icon = iconForSport(workout.sportType);
|
||||
const sportLabelKey = `insights.workouts.sport.${workout.sportType}`;
|
||||
const sportLabel = t(sportLabelKey);
|
||||
// Translation keys missing from one locale fall back to the
|
||||
// canonical sport-type string so a new HK enum value never
|
||||
// renders the raw `insights.workouts.sport.x` placeholder.
|
||||
const sportName =
|
||||
sportLabel === sportLabelKey ? workout.sportType : sportLabel;
|
||||
translateOrNull(t, sportLabelKey) ?? workout.sportType;
|
||||
|
||||
return (
|
||||
// v1.42 — the row opens the workout and its date opens the day:
|
||||
|
||||
Reference in new issue
Block a user