feat: add <RelativeTime> component for relative date/time formatting#1153
feat: add <RelativeTime> component for relative date/time formatting#1153moss-bryophyta wants to merge 16 commits intogeneraltranslation:mainfrom
Conversation
Code Review —
|
|
@greptile-ai Please re-review this PR — the latest push addresses all feedback from the previous review (floor-based unit selection, date prop API, explicit options merging, RTL fix, name prop documentation, and additional edge case tests). |
- Add template for RelativeTime component (gt-next, gt-react) - Add auto-generated stubs for next and react docs - Add core standalone function docs for formatRelativeTime and formatRelativeTimeFromDate - Add core class method docs for formatRelativeTime and formatRelativeTimeFromDate - Update all relevant meta.json navigation files Covers PR generaltranslation/gt#1153
Adds a new <RelativeTime> React component that renders localized relative
time strings (e.g. '2 hours ago', 'in 3 days') using Intl.RelativeTimeFormat.
Core changes:
- _selectRelativeTimeUnit(): auto-selects best unit from a Date
- _formatRelativeTimeFromDate(): formats relative time from a Date
- GT.formatRelativeTimeFromDate(): class method
- formatRelativeTimeFromDate(): standalone function
Component (two modes):
- <RelativeTime>{someDate}</RelativeTime> — auto-selects unit from Date
- <RelativeTime value={-1} unit='day' /> — explicit value + unit
Added to: react-core, gt-react, gt-next (server + client + types),
gt-tanstack-start.
Closes generaltranslation#1152
- Add comprehensive tests for _selectRelativeTimeUnit, _formatRelativeTimeFromDate, and _formatRelativeTime - Add week to auto-selection thresholds (7-27 days → weeks) - Add changeset for minor bump across all affected packages
- Use Math.floor instead of Math.round to avoid confusing jumps near
unit boundaries (e.g. 3.5 days showing as '1 week ago')
- Change primary API from children to date prop (<RelativeTime date={d} />)
with children kept for backwards compat
- Document name prop (used by GT CLI for extraction context)
- Remove RTL character stripping (was breaking bidi text in Arabic/Hebrew)
- Fix options merging to explicitly pass numeric/style instead of spreading
(prevents accidental locales override)
- Add edge case tests: zero diff, month boundary, floor behavior
- Add explanatory comment on Spanish locale test variance
- CLI: add RelativeTime to GT_TRANSLATION_FUNCS and VARIABLE_COMPONENTS
- Compiler: add RelativeTime to GT_COMPONENT_TYPES enum, MINIFY_CANONICAL_NAME_MAP ('rt'),
isGTComponent, isVariableComponent, and defaultVariableNames
- SWC plugin: add RelativeTime to get_variable_type (as Date type),
is_variable_component_name, and related tests
- Update changeset to include gt (CLI) and @generaltranslation/compiler
- Compiler: test isGTComponent, isVariableComponent, isDeriveComponent, GT_COMPONENT_TYPES enum, MINIFY_CANONICAL_NAME_MAP, defaultVariableNames, and getVariableName all recognize RelativeTime - CLI: test GT_TRANSLATION_FUNCS and VARIABLE_COMPONENTS include RelativeTime
…, /client export, hydration safety - Extract reused magic strings in constants.ts to named constants - Add 'relative-time' to VariableTransformationSuffix, Transformation, VariableType, and minify map - Add RelativeTime to renderVariable for hot reload support - Add RelativeTime default variable name in getVariableName - Export RelativeTime from gt-react /client - Replace Date.now() with baseDate parameter in _selectRelativeTimeUnit for hydration safety - Thread baseDate through formatRelativeTimeFromDate, GT class, and both RelativeTime components (react-core + next) - Add baseDate to next index.types.ts JSDoc
… warning - Restore JSDoc for _formatRelativeTime (was orphaned by insertion) - Fix edge case where 360-364 days showed 'this year' instead of months - Add dev-mode warning when value is provided without unit in RelativeTime
- Hydration safety: remove Date.now() default from core _selectRelativeTimeUnit, require baseDate parameter. Components default to new Date() at render time. - Fix prettier/lint errors in renderVariable.tsx and console.warn statements - Add test for 360-364 day edge case (months=12, years=0) - Update all test calls to pass explicit baseDate
ff9c008 to
6838278
Compare
|
@greptile-ai please review again |
- Fix P1: variable name mismatch — change 'date' to 'time' in getVariableName.ts
to match compiler's defaultVariableNames for RelativeTime
- Add dedicated VariableType::RelativeTime ('rt') to SWC plugin instead of
reusing VariableType::Date
- Forward localeMatcher option in both react-core and next RelativeTime components
- Fix prettier formatting in core and CLI test files
|
@greptile-ai please review again |
… renderVariable - Add VariableType::RelativeTime match arm in traversal.rs - Coerce string/number values to Date in renderVariable for RelativeTime, matching how DateTime handles non-Date inputs gracefully
|
@claude review |
There was a problem hiding this comment.
Code review is billed via overage credits. To resume reviews, an organization admin can raise the monthly limit at claude.ai/admin-settings/claude-code.
Once credits are available, comment @claude review on this pull request to trigger a review.
|
Claude encountered an error —— View job I'll analyze this and get back to you. |
|
@greptile-ai re-review |
When days=28-29, months=floor(days/30)=0, causing 'this month' instead of the expected week value. Added months<1 guard (same pattern as years<1 guard for 360-364 days). Added test coverage for both 28 and 29 day cases.
|
@greptile-ai All findings from your latest review have been addressed: P1 — 28-29 day boundary dead zone: Fixed by adding a P1 — Silent null for non-Date values (from prior review): Already fixed — The full boundary logic is now: Please re-review when ready. |
Destructure locales and baseDate from options before passing to _formatRelativeTimeFromDate, so only genuine Intl.RelativeTimeFormatOptions (numeric, style, localeMatcher) reach the cache key.
|
@greptile-ai Fixed the P1 cache pollution bug:
Fix: Destructured The React components were already safe (they manually pick only known Intl options), so this was isolated to the standalone function path. Please re-review. |
Wrap output in <span suppressHydrationWarning> to handle the inherent SSR/client time difference gracefully, matching the standard React pattern for time-dependent content.
|
@greptile-ai Added |
RelativeTime can't be a span (it's a text node), so suppressHydrationWarning doesn't work on it. Remove the span and return a fragment like DateTime does.
|
@greptile-ai re-review |
Summary
Adds a
<RelativeTime>component that renders localized relative time strings usingIntl.RelativeTimeFormat. Closes #1152.Inspired by community request and GT's existing
formatRelativeTime()in core.Changes
Core (
generaltranslation)_selectRelativeTimeUnit(date, baseDate)— auto-selects the best unit (seconds → years) based on the difference between a targetDateand a base date.baseDateis required (no internalDate.now()default) for hydration safety._formatRelativeTimeFromDate({ date, baseDate, locales, options })— formats relative time from aDateusing auto-unit selectionGT.formatRelativeTimeFromDate(date, options)— class method on theGTclassformatRelativeTimeFromDate(date, options)— standalone exported functionVariableType 'rt'— new minified variable type for relative-timeVariableTransformationSuffix 'relative-time'— new transformation suffixReact Components
New
<RelativeTime>component with two usage modes:Props:
date(Date),value(number),unit(RelativeTimeFormatUnit),baseDate(Date, for hydration safety),name(string),locales(string[]),options(Intl.RelativeTimeFormatOptions includinglocaleMatcher)Uses
_gtt = 'variable-relative-time'tag for the GT transformation system.Packages updated
generaltranslation(core)formatRelativeTimeFromDate, types@generaltranslation/react-core<RelativeTime>component,renderVariablesupport,getVariableNamegt-react/clientgt-nextVariableType::RelativeTime)gt(CLI)RelativeTimecomponent detection@generaltranslation/compiler'time')Auto-unit selection thresholds
secondminutehourdayweekmonthyearHydration safety
_selectRelativeTimeUnitrequires an explicitbaseDateparameter — it does not callDate.now()internally. The<RelativeTime>component defaultsbaseDatetonew Date()at render time, ensuring server and client use the same timestamp during hydration.Greptile Summary
This PR adds a
<RelativeTime>component for localized relative-time formatting (e.g. "2 hours ago", "in 3 days") across the entire GT stack — core library, React, Next.js, and TanStack Start — usingIntl.RelativeTimeFormatunder the hood. It introduces auto-unit selection logic, a new'rt'variable type, corresponding transformations, and all necessary compiler/SWC-plugin wiring to treatRelativeTimeas a first-class variable component alongsideDateTime,Num, etc.years=0edge case is fixed with an explicitif (years < 1)guard;baseDateis destructured out ofintlOptionsbefore it reaches the Intl cache;value-without-unitemits a dev-modeconsole.warn; andrenderVariablecoerces strings/numbers toDatewith anisNaNguard.baseDateoption — important for SSR hydration safety — is not listed in the@paramJSDoc for the publicformatRelativeTimeFromDatestandalone function orGT.formatRelativeTimeFromDateclass method. IDE consumers won't see it in hover docs.baseDateprop is provided as the escape hatch for users who need strict determinism.Confidence Score: 5/5
Safe to merge — all previously raised correctness concerns are resolved and only a minor JSDoc gap remains.
All P0/P1 issues from prior review threads have been addressed: the 360-364 day edge case, the baseDate Intl-cache leak, the value-without-unit silent failure, and the renderVariable coercion. The only remaining finding is a P2 documentation gap (missing @param for baseDate in public API JSDoc) that does not affect runtime behaviour.
packages/core/src/index.ts — JSDoc for
formatRelativeTimeFromDate(standalone) andGT.formatRelativeTimeFromDateis missing a@paramentry forbaseDate.Important Files Changed
_selectRelativeTimeUnitand_formatRelativeTimeFromDate; unit-boundary edge cases (28-29 day, 360-364 day) are correctly handled;baseDateis not leaked into the Intl cache.formatRelativeTimeFromDatestandalone function andGT.formatRelativeTimeFromDateclass method;baseDateis correctly destructured before being forwarded to Intl, butbaseDateis missing from the JSDoc@paramblock for both the class method and standalone function.date(auto-unit) andvalue+unit(explicit) modes; dev-mode warning forvaluewithoutunit; hydration caveat is acknowledged in comments.useLocale()correctly following the existing DateTime pattern.'rt'variable type handling with proper coercion from string/number to Date and NaN guard before passing to RelativeTime.'rt'toVariableType; multi-character minified name is intentional to avoid collision with existing single-character types.RelativeTimevariant with serde rename"rt"to the RustVariableTypeenum, consistent with the TypeScript side.RelativeTimetoGT_COMPONENT_TYPESenum and maps it to'rt'inMINIFY_CANONICAL_NAME_MAP; changes are consistent with other variable components.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A["<RelativeTime date={d} />\nor value+unit"] --> B{Which mode?} B -- "value + unit" --> C["gt.formatRelativeTime(value, unit, options)"] B -- "date prop" --> D["gt.formatRelativeTimeFromDate(date, options)"] D --> E["formatRelativeTimeFromDate (standalone)\ndestructures: locales, baseDate, ...intlOptions"] E --> F["_formatRelativeTimeFromDate(date, baseDate, locales, intlOptions)"] F --> G["_selectRelativeTimeUnit(date, baseDate)\n→ {value, unit}"] G --> H["_formatRelativeTime(value, unit, locales, intlOptions)"] C --> H H --> I["intlCache.get('RelativeTimeFormat', locales, intlOptions)\n(baseDate NOT included in cache key)"] I --> J["Intl.RelativeTimeFormat.format(value, unit)"] J --> K["Rendered string: '2 hours ago'"] style E fill:#d4edda style I fill:#d4eddaPrompt To Fix All With AI
Reviews (8): Last reviewed commit: "Add hydration error comments to DateTime..." | Re-trigger Greptile