Skip to main content

The Challenge

SMO1 needs cat mode (playful: “73 Pawprintz”, “Zoomies ⚡”) and professional mode (corporate: “73 Activity”, “Active”) labels in the same components. Naive approaches create tech debt:
  • ❌ Two separate component files (CatScoringChips + ProScoringChips) → duplicate logic, divergence
  • ❌ Inline ternaries everywhere → noisy, hard to audit all terminology
  • ❌ Config file in backend → adds API surface, complicates deployments

Solution: Terminology Mapping + Single Component

Step 1: Extract Label Pairs into Mapping Objects

// Inside ScoringChips component file
const CAT_LABELS = {
  pawprintz: "Pawprintz",
  treatz: "Treatz",
  niblz: "Niblz",
};

const PRO_LABELS = {
  pawprintz: "Activity",
  treatz: "Efficiency",
  niblz: "Compression",
};

Step 2: Create Helper Function

function getMetricLabel(metric: "pawprintz" | "treatz" | "niblz", isCatMode: boolean): string {
  const labels = isCatMode ? CAT_LABELS : PRO_LABELS;
  return labels[metric];
}

Step 3: Use in Component

export function ScoringChips({ pawprintz, treatz, niblz }: ScoringChipsProps) {
  const { isCatMode } = useCatMode();

  return (
    <div className="flex gap-2">
      <span>{pawprintz} {getMetricLabel("pawprintz", isCatMode)}</span>
      <span>{treatz} {getMetricLabel("treatz", isCatMode)}</span>
      <span>{niblz} {getMetricLabel("niblz", isCatMode)}</span>
    </div>
  );
}

Step 4: Test Both Modes

test("ScoringChips renders cat mode labels", () => {
  const { getByText } = render(
    <CatModeProvider>
      <ScoringChips pawprintz={73} treatz={42} niblz={15} />
    </CatModeProvider>
  );
  
  expect(getByText("Pawprintz")).toBeInTheDocument();
  expect(getByText("Treatz")).toBeInTheDocument();
});

test("ScoringChips renders professional mode labels", () => {
  localStorage.setItem("appearance-cat-mode", "false");
  const { getByText } = render(
    <CatModeProvider>
      <ScoringChips pawprintz={73} treatz={42} niblz={15} />
    </CatModeProvider>
  );
  
  expect(getByText("Activity")).toBeInTheDocument();
  expect(getByText("Efficiency")).toBeInTheDocument();
});

Advanced: Status Badges with Emojis

For mappings where cat mode has emojis but professional doesn’t:
type Status = "zoomies" | "sleepy" | "loaf";

const STATUS_LABELS: Record<Status, { cat: string; pro: string }> = {
  zoomies: { cat: "Zoomies ⚡", pro: "Active" },
  sleepy: { cat: "Snoozies 😴", pro: "Resting" },
  loaf: { cat: "Loaf 🍞", pro: "Dormant" },
};

function getStatusLabel(status: Status, isCatMode: boolean): string {
  return isCatMode ? STATUS_LABELS[status].cat : STATUS_LABELS[status].pro;
}

export function StatusBadge({ status }: { status: Status }) {
  const { isCatMode } = useCatMode();
  const label = getStatusLabel(status, isCatMode);
  
  return <span className="badge">{label}</span>;
}

At Scale: Terminology Registry

For projects with dozens of terminology pairs, centralize in a TypeScript file:
// lib/terminology.ts
export const TERMINOLOGY = {
  metrics: {
    pawprintz: { cat: "Pawprintz", pro: "Activity" },
    treatz: { cat: "Treatz", pro: "Efficiency" },
    niblz: { cat: "Niblz", pro: "Compression" },
  },
  status: {
    zoomies: { cat: "Zoomies ⚡", pro: "Active" },
    sleepy: { cat: "Snoozies 😴", pro: "Resting" },
    loaf: { cat: "Loaf 🍞", pro: "Dormant" },
  },
  time_range: {
    week: { cat: "Weekly Pounces", pro: "7 Days" },
    month: { cat: "Monthly Prowl", pro: "30 Days" },
    quarter: { cat: "Nine Lives", pro: "90 Days" },
    all: { cat: "All Whiskers", pro: "All Time" },
  },
} as const;

// Usage:
const label = TERMINOLOGY.metrics.pawprintz[isCatMode ? "cat" : "pro"];

Comparison: Other Approaches

ApproachCode DuplicationMaintenanceTestingPerformance
Separate componentsHigh ❌Hard ❌Complex ❌Same
Inline ternariesNone ✅Hard ❌Simple ✅Same
Terminology mappingNone ✅Easy ✅Simple ✅Same ✅
i18n libraryNone ✅Easy ✅Simple ✅Slight overhead
Backend configNone ✅Complex ❌Flaky ❌Network latency ❌

Key Wins

Auditable: All terminology pairs in one place (or one file per feature) ✅ Testable: Add tests incrementally as new terminology is added ✅ No Duplication: Component logic shared between modes ✅ Scalable: Extend terminology object without touching component code ✅ Localizable: If you ever need i18n, translation keys already structured

Gotchas

Forgetting a pair: If you add a cat label but forget the pro label, runtime errors reveal it quickly. Use TypeScript as const for stricter types. ❌ Terminology drift: Terminology file becomes the source of truth. Keep it near components it serves (not buried in utils). ❌ Emoji placement: Emojis should be in the string (not separate DOM), so they stay synchronized with labels during mode changes.

When to Reach for i18n

Use this pattern if terminology is regional (English vs Spanish) AND mode-specific (cat vs pro). Then layer them:
const label = TERMINOLOGY.metrics[metric][isCatMode ? "cat" : "pro"][languageCode];
Otherwise, for single-language multi-mode, this pattern is leaner than i18n.