diff --git a/apps/frontend/AGENTS.md b/apps/frontend/AGENTS.md index b677a79f9b4..179a7a5e9d2 100644 --- a/apps/frontend/AGENTS.md +++ b/apps/frontend/AGENTS.md @@ -28,11 +28,13 @@ Nuxt 3 with SSR — pages are server-rendered and hydrated on the client. Uses ` Rule of thumb: if it doesn't depend on Nuxt-specific APIs or website-only features, it should be in `packages/ui`. -### Project Review Popout Windows +### Localization -Project review panels can be displayed in multiple browser documents while sharing the main window's Vue application and JavaScript context. Use an anchor, panel, or event target's `ownerDocument` for DOM queries and its `defaultView` for window behavior. Global `document` and `window` still identify the main window after a panel moves. +Any admin or internal pages should not be localized. Use plain English strings, ordinary interpolation and plural handling, and English date/number formatting in their routes, components, providers, and supporting code. This exception also applies during i18n passes. Preserve localization in shared components used elsewhere. -Use the review shortcut context's listeners (`shortcuts.listen` or `useReviewEventListener`) for review interactions that must work in both the workspace and popouts. Shared overlay and modal behavior follows the multiple-document guidance in `packages/ui/AGENTS.md`. +### Popout Windows + +See `standards/frontend/POPOUT-WINDOWS.md` for DOM ownership, event listeners, and overlay behavior across browser documents. ## Data Fetching diff --git a/apps/frontend/src/components/project-review/description/index.vue b/apps/frontend/src/components/project-review/description/index.vue index e117c609936..abfcc933af0 100644 --- a/apps/frontend/src/components/project-review/description/index.vue +++ b/apps/frontend/src/components/project-review/description/index.vue @@ -16,9 +16,7 @@ />
-

- {{ formatMessage(messages.empty) }} -

-

- {{ formatMessage(messages.loading) }} -

+

No project selected

+

Loading...

-

{{ formatMessage(messages.loadError) }}

- +

Could not load project data

+
-

- {{ formatMessage(messages.emptyDescription) }} -

+

No description

+ + + + diff --git a/apps/frontend/src/components/project-review/format.ts b/apps/frontend/src/components/project-review/format.ts new file mode 100644 index 00000000000..c9c5ddaf9a9 --- /dev/null +++ b/apps/frontend/src/components/project-review/format.ts @@ -0,0 +1,58 @@ +const numberFormatter = new Intl.NumberFormat('en-US') +const compactFormatter = new Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 2, +}) +const shortCompactFormatter = new Intl.NumberFormat('en-US', { + notation: 'compact', + maximumFractionDigits: 1, +}) + +export const formatNumber = (value: number | bigint) => numberFormatter.format(value) +export const formatCompactNumber = (value: number | bigint) => { + if (value < 10_000) return formatNumber(value) + return (value < 1_000_000 ? shortCompactFormatter : compactFormatter).format(value) +} + +export function formatBytes(bytes: number, decimals = 2) { + if (bytes === 0) return '0 bytes' + const units = ['bytes', 'KiB', 'MiB', 'GiB', 'TiB'] + const exponent = Math.max( + 0, + Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1), + ) + const value = Number((bytes / 1024 ** exponent).toFixed(decimals)) + return `${formatNumber(value)} ${exponent === 0 && value === 1 ? 'byte' : units[exponent]}` +} + +export function createDateFormatter(options?: Intl.DateTimeFormatOptions) { + const formatter = new Intl.DateTimeFormat('en-US', options) + return (value: Date | number | string) => formatter.format(new Date(value)) +} + +export function createRelativeTimeFormatter(options?: Intl.RelativeTimeFormatOptions) { + const formatter = new Intl.RelativeTimeFormat('en-US', { + numeric: 'auto', + ...options, + }) + return (value: Date | number | string | null | undefined) => { + if (value == null) return '' + const timestamp = new Date(value).getTime() + if (Number.isNaN(timestamp)) return '' + const difference = timestamp - Date.now() + const units: [Intl.RelativeTimeFormatUnit, number, number][] = [ + ['second', 1000, 60], + ['minute', 60_000, 60], + ['hour', 3_600_000, 24], + ['day', 86_400_000, 7], + ['week', 604_800_000, 4], + ['month', 2_629_746_000, 12], + ['year', 31_556_952_000, Infinity], + ] + for (const [unit, duration, limit] of units) { + const amount = Math.round(difference / duration) + if (Math.abs(amount) < limit) return formatter.format(amount, unit) + } + return '' + } +} diff --git a/apps/frontend/src/components/project-review/gallery/index.vue b/apps/frontend/src/components/project-review/gallery/index.vue index df30e8fe248..6e38d56e2a7 100644 --- a/apps/frontend/src/components/project-review/gallery/index.vue +++ b/apps/frontend/src/components/project-review/gallery/index.vue @@ -9,20 +9,18 @@ class="shrink-0" />
-

- {{ formatMessage(messages.empty) }} -

-

- {{ formatMessage(messages.loading) }} -

+

No project selected

+

Loading...

-

{{ formatMessage(messages.loadError) }}

- +

Could not load project data

+
-

- {{ formatMessage(messages.emptyGallery) }} -

-