Files
ServerKit/frontend/src/utils/formatBytes.js
T
Juan DenisandClaude Opus 5 7050b24137 feat(i18n): one locale-aware format door; delete the timeAgo shim
Plan 79 §C + ratchet 5.

utils/intl.js owns dates, times, relative time, durations, numbers, percents
and lists. Three things the scattered calls could not do:

- A module-level locale published by LocaleContext, not read from a hook, so
  the API error mapper, log formatter and CSV export format in the panel's
  language too. useFormat() is a subscription over the same module, not a
  second door.
- Formatter instances cached per (locale, kind, options). The
  toLocaleDateString() calls this replaces built a fresh formatter per cell
  per render, and Intl construction is the expensive half.
- Relative time via Intl.RelativeTimeFormat rather than translation keys --
  the browser already carries every language's plural wording, and keying
  "{{count}} days ago" would mean maintaining that table by hand.

This fixes a locale bug that predates any translation work: 124 call sites
use toLocale*() with no argument, which follows the BROWSER's locale, so a
panel set to Spanish already rendered English chrome around Spanish dates.
formatNumber.js was worse -- it built Intl.NumberFormat('en') at module load,
hardcoding English AND making it impossible to follow a switch.

The three human-facing utils now delegate instead of reimplementing, so their
existing callers become locale-aware without being touched. formatBytes gains
the locale decimal separator ("1,5 KB" in es) by replacing a toFixed + regex
trim with maximumFractionDigits, which trims identically in en -- parity
proved case by case against the pre-migration examples.

utils/timeAgo.js is deleted rather than left as a shim; its 6 importers move
to utils/time. A door nobody is forced through is a door nobody uses.

One bug found by the tests: formatNumber(null) returned "0", because
Number(null) is 0 -- a missing metric would have rendered as a real zero.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 23:22:28 -04:00

60 lines
2.3 KiB
JavaScript

// Canonical byte/size formatter for the whole frontend.
//
// Replaces the ~14 local `formatBytes` / `formatSize` / `formatMemory`
// implementations that had drifted apart. The codebase universally treats
// sizes as binary (divide by 1024) with conventional KB/MB/GB labels, so that
// is the default here — displayed values stay identical after migration.
//
// formatBytes(1536) -> "1.5 KB"
// formatBytes(0) -> "0 B"
// formatBytes(null) -> "-"
// formatBytes(1234567, { decimals: 2 }) -> "1.18 MB"
// formatBytes(2048, { iec: true }) -> "2 KiB"
// formatBytes(2048, { suffix: false }) -> "2"
import { formatNumber } from './intl.js';
const DECIMAL_UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
const IEC_UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
export function formatBytes(bytes, options = {}) {
const {
decimals = 1,
suffix = true,
iec = false, // true -> KiB/MiB/GiB labels (divisor is always 1024)
defaultValue = '-',
} = options;
if (bytes === null || bytes === undefined || bytes === '') return defaultValue;
const value = typeof bytes === 'string' ? Number(bytes) : bytes;
if (!Number.isFinite(value)) return defaultValue;
if (value === 0) return suffix ? '0 B' : '0';
const units = iec ? IEC_UNITS : DECIMAL_UNITS;
const negative = value < 0;
const abs = Math.abs(value);
const exponent = Math.min(
Math.floor(Math.log(abs) / Math.log(1024)),
units.length - 1
);
const scaled = abs / 1024 ** exponent;
// Whole-byte values never need decimals.
const places = exponent === 0 ? 0 : decimals;
// Plan 79 §C: through the format door rather than toFixed + a regex trim.
// `maximumFractionDigits` drops trailing zeros for free ("1.0" -> "1",
// "1.50" -> "1.5") AND uses the locale's decimal separator, which the
// string-trim could not: a Spanish panel showed "1.5 KB" where every other
// number on the page read "1,5".
const formatted = formatNumber(scaled, { maximumFractionDigits: places });
const sign = negative ? '-' : '';
return suffix ? `${sign}${formatted} ${units[exponent]}` : `${sign}${formatted}`;
}
export default formatBytes;