mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-12 15:03:55 +09:00
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>
60 lines
2.3 KiB
JavaScript
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;
|