From b710fa9f60cfe270bea4bd827d7c9f211cfe9b74 Mon Sep 17 00:00:00 2001 From: MartinBraquet Date: Mon, 23 Feb 2026 23:49:51 +0100 Subject: [PATCH] Finish notif translations --- backend/api/src/create-comment.ts | 6 +- backend/api/src/create-notification.ts | 95 +++++-------------- backend/api/src/get-notifications.ts | 41 +++++++- .../shared/src/create-profile-notification.ts | 3 +- backend/shared/src/supabase/notifications.ts | 73 +++++++++----- common/messages/de.json | 13 +++ common/messages/fr.json | 12 +++ common/src/notifications.ts | 73 +++++++------- tests/e2e/utils/seed-test-data.ts | 11 ++- web/components/notification-items.tsx | 24 +++-- web/components/relative-timestamp.tsx | 5 +- web/hooks/use-notifications.ts | 7 +- web/lib/util/shortenedFromNow.ts | 9 +- web/pages/notifications.tsx | 2 - 14 files changed, 217 insertions(+), 157 deletions(-) diff --git a/backend/api/src/create-comment.ts b/backend/api/src/create-comment.ts index e28bf01c..3c924fad 100644 --- a/backend/api/src/create-comment.ts +++ b/backend/api/src/create-comment.ts @@ -8,7 +8,7 @@ import {getNotificationDestinationsForUser} from 'common/user-notification-prefe import {richTextToString} from 'common/util/parse' import * as crypto from 'crypto' import {sendNewEndorsementEmail} from 'email/functions/helpers' -import {createSupabaseDirectClient, SupabaseDirectClient} from 'shared/supabase/init' +import {createSupabaseDirectClient} from 'shared/supabase/init' import {insertNotificationToSupabase} from 'shared/supabase/notifications' import {getPrivateUser, getUser} from 'shared/utils' import {broadcastUpdatedComment} from 'shared/websockets/helpers' @@ -44,7 +44,6 @@ export const createComment: APIHandler<'create-comment'> = async ( creator, richTextToString(content), comment.id, - pg, ) broadcastUpdatedComment(convertComment(comment)) @@ -78,7 +77,6 @@ const createNewCommentOnProfileNotification = async ( creator: User, sourceText: string, commentId: number, - pg: SupabaseDirectClient, ) => { const privateUser = await getPrivateUser(onUser.id) if (!privateUser) return @@ -104,7 +102,7 @@ const createNewCommentOnProfileNotification = async ( sourceSlug: onUser.username, } if (sendToBrowser) { - await insertNotificationToSupabase(notification, pg) + await insertNotificationToSupabase(notification) } if (sendToMobile) { // await createPushNotification( diff --git a/backend/api/src/create-notification.ts b/backend/api/src/create-notification.ts index b3ac49ad..d11ade33 100644 --- a/backend/api/src/create-notification.ts +++ b/backend/api/src/create-notification.ts @@ -9,6 +9,9 @@ import { NotificationTemplateTranslation, } from 'shared/supabase/notifications' +const COMPASS_LOGO_URL = + 'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fcompass-192.png?alt=media&token=9fd251c5-fc43-4375-b629-1a8f4bbe8185' + export const createAndroidReleaseNotifications = async () => { const createdTime = Date.now() const id = `android-release-${createdTime}` @@ -20,8 +23,7 @@ export const createAndroidReleaseNotifications = async () => { sourceType: 'info', sourceUpdateType: 'created', sourceSlug: ANDROID_APP_URL, - sourceUserAvatarUrl: - 'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fcompass-192.png?alt=media&token=9fd251c5-fc43-4375-b629-1a8f4bbe8185', + sourceUserAvatarUrl: COMPASS_LOGO_URL, title: 'Android App Released on Google Play', sourceText: 'The Compass Android app is now publicly available on Google Play! Download it today to stay connected on the go.', @@ -40,8 +42,7 @@ export const createAndroidTestNotifications = async () => { sourceType: 'info', sourceUpdateType: 'created', sourceSlug: '/contact', - sourceUserAvatarUrl: - 'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fcompass-192.png?alt=media&token=9fd251c5-fc43-4375-b629-1a8f4bbe8185', + sourceUserAvatarUrl: COMPASS_LOGO_URL, title: 'Android App Ready for Review — Help Us Unlock the Google Play Release', sourceText: 'To release our app, Google requires a closed test with at least 12 testers for 14 days. Please share your Google Play–registered email address so we can add you as a tester and complete the review process.', @@ -129,38 +130,16 @@ export const createNotification = async ( * Uses the new template-based system for efficient bulk notifications */ export const createEventsAvailableNotifications = async () => { - const pg = createSupabaseDirectClient() - - // Fetch all users - const {data: users, error} = await tryCatch(pg.many>('select id from users')) - - if (error) { - console.error('Error fetching users', error) - return {success: false, error} - } - - if (!users || users.length === 0) { - console.error('No users found') - return {success: false, error: 'No users found'} - } - - const userIds = users.map((u) => u.id) - // Create template and bulk notifications using the new system - const {templateId, count} = await createBulkNotification( - { - sourceType: 'info', - title: 'New Events Page', - sourceText: - 'You can now create and join events on Compass! Meet up with other members online or in person for workshops, social events, etc.', - sourceSlug: '/events', - sourceUserAvatarUrl: - 'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fcompass-192.png?alt=media&token=9fd251c5-fc43-4375-b629-1a8f4bbe8185', - sourceUpdateType: 'created', - }, - userIds, - pg, - ) + const {templateId, count} = await createBulkNotification({ + sourceType: 'info', + title: 'New Events Page', + sourceText: + 'You can now create and join events on Compass! Meet up with other members online or in person for workshops, social events, etc.', + sourceSlug: '/events', + sourceUserAvatarUrl: COMPASS_LOGO_URL, + sourceUpdateType: 'created', + }) console.log(`Created events notification template ${templateId} for ${count} users`) @@ -172,58 +151,32 @@ export const createEventsAvailableNotifications = async () => { } export const createSomeNotifications = async () => { - const pg = createSupabaseDirectClient() - - // Fetch all users - const {data: users, error} = await tryCatch(pg.many>('select id from users')) - - if (error) { - console.error('Error fetching users', error) - return {success: false, error} - } - - if (!users || users.length === 0) { - console.error('No users found') - return {success: false, error: 'No users found'} - } - - const userIds = users.map((u: Row<'users'>) => u.id) - const translations: Omit[] = [ // French translation { locale: 'fr', - title: '', - source_text: '', + title: 'Bonjour', + source_text: "C'est une notif", }, // German translation { locale: 'de', - title: '', - source_text: '', + title: 'Halo', + source_text: 'Dis das', }, ] // Create template with translations const {templateId, count} = await createBulkNotification( { - sourceType: 'welcome', - title: '', - sourceText: '', - sourceSlug: '/', - sourceUserAvatarUrl: '', + sourceType: 'hello', + title: 'Hello world', + sourceText: 'This is a notification', + sourceSlug: '/settings', + sourceUserAvatarUrl: COMPASS_LOGO_URL, sourceUpdateType: 'created', }, - userIds, - pg, translations, ) - - console.log(`Created notification template ${templateId} for ${count} users`) - - return { - success: true, - templateId, - userCount: count, - } + console.log(`Created some notification template ${templateId} for ${count} users`) } diff --git a/backend/api/src/get-notifications.ts b/backend/api/src/get-notifications.ts index 5d791387..21f8202a 100644 --- a/backend/api/src/get-notifications.ts +++ b/backend/api/src/get-notifications.ts @@ -3,6 +3,18 @@ import {defaultLocale} from 'common/constants' import {Notification} from 'common/notifications' import {createSupabaseDirectClient} from 'shared/supabase/init' +// Helper function to substitute placeholders in template text +function substitutePlaceholders(templateText: string, templateData: any): string { + let result = templateText + if (templateData) { + for (const [key, value] of Object.entries(templateData)) { + // Replace all occurrences of {key} with the value + result = result.replace(new RegExp(`\\{${key}\\}`, 'g'), String(value)) + } + } + return result +} + export const getNotifications: APIHandler<'get-notifications'> = async (props, auth, _req) => { const {limit, after, locale = defaultLocale} = props const pg = createSupabaseDirectClient() @@ -23,7 +35,8 @@ export const getNotifications: APIHandler<'get-notifications'> = async (props, a 'sourceText', COALESCE(ntt.source_text, nt.source_text), 'sourceSlug', nt.source_slug, 'sourceUserAvatarUrl', nt.source_user_avatar_url, - 'data', nt.data + 'data', nt.data, + 'templateData', un.data->'templateData' ) else un.data @@ -47,9 +60,31 @@ export const getNotifications: APIHandler<'get-notifications'> = async (props, a limit $2 ` - return await pg.map( + const rawNotifications = await pg.map( query, [auth.uid, limit, after, locale], - (row) => row.notification_data as Notification, + (row) => row.notification_data, ) + + // Process notifications to apply template data substitution + const processedNotifications: Notification[] = rawNotifications.map((notif: any) => { + if (notif.templateId) { + // Apply template data substitution to title and sourceText + const templateData = notif.templateData || {} + const processedNotif = {...notif} + + if (processedNotif.title) { + processedNotif.title = substitutePlaceholders(processedNotif.title, templateData) + } + + if (processedNotif.sourceText) { + processedNotif.sourceText = substitutePlaceholders(processedNotif.sourceText, templateData) + } + + return processedNotif as Notification + } + return notif as Notification + }) + + return processedNotifications } diff --git a/backend/shared/src/create-profile-notification.ts b/backend/shared/src/create-profile-notification.ts index de296ba9..de1c1fbc 100644 --- a/backend/shared/src/create-profile-notification.ts +++ b/backend/shared/src/create-profile-notification.ts @@ -9,7 +9,6 @@ import {getPrivateUser, getUser} from './utils' export const createProfileLikeNotification = async (like: Row<'profile_likes'>) => { const {creator_id, target_id, like_id} = like - const pg = createSupabaseDirectClient() const targetPrivateUser = await getPrivateUser(target_id) const profile = await getProfile(creator_id) @@ -34,7 +33,7 @@ export const createProfileLikeNotification = async (like: Row<'profile_likes'>) sourceUserAvatarUrl: profile.pinned_url ?? profile.user.avatarUrl, sourceText: '', } - return await insertNotificationToSupabase(notification, pg) + return await insertNotificationToSupabase(notification) } export const createProfileShipNotification = async ( diff --git a/backend/shared/src/supabase/notifications.ts b/backend/shared/src/supabase/notifications.ts index 1733758f..180e0f01 100644 --- a/backend/shared/src/supabase/notifications.ts +++ b/backend/shared/src/supabase/notifications.ts @@ -1,6 +1,7 @@ import {Notification, NotificationTemplate} from 'common/notifications' import {Row} from 'common/supabase/utils' -import {SupabaseDirectClient} from 'shared/supabase/init' +import {tryCatch} from 'common/util/try-catch' +import {createSupabaseDirectClient, SupabaseDirectClient} from 'shared/supabase/init' import {bulkInsert} from 'shared/supabase/utils' import {broadcast} from 'shared/websockets/server' @@ -16,8 +17,9 @@ export type NotificationTemplateTranslation = Row<'notification_template_transla */ export const insertNotificationToSupabase = async ( notification: Notification, - pg: SupabaseDirectClient, + pg?: SupabaseDirectClient, ) => { + pg = pg ?? createSupabaseDirectClient() // Check if this notification has a template_id (new style) if (notification.templateId) { await pg.none( @@ -31,6 +33,7 @@ export const insertNotificationToSupabase = async ( { isSeen: notification.isSeen, viewTime: notification.viewTime, + templateData: notification.templateData || {}, // Store dynamic template data }, ], ) @@ -73,8 +76,8 @@ export const bulkInsertNotifications = async ( */ export const createNotificationTemplate = async ( template: NotificationTemplate, - pg: SupabaseDirectClient, ): Promise => { + const pg = createSupabaseDirectClient() await pg.none( `insert into notification_templates (id, source_type, title, source_text, source_slug, source_user_avatar_url, source_update_type, created_time, data) @@ -106,9 +109,9 @@ export const createNotificationTemplate = async ( */ export const createNotificationTemplateTranslations = async ( translations: NotificationTemplateTranslation[], - pg: SupabaseDirectClient, ): Promise => { if (translations.length === 0) return + const pg = createSupabaseDirectClient() await bulkInsert(pg, 'notification_template_translations', translations) } @@ -120,10 +123,9 @@ export const createNotificationTemplateTranslations = async ( export const createNotificationTemplateWithTranslations = async ( template: NotificationTemplate, translations: Omit[], - pg: SupabaseDirectClient, ): Promise => { // Create the base template - await createNotificationTemplate(template, pg) + await createNotificationTemplate(template) // Add template_id to translations and create them if (translations.length > 0) { @@ -132,11 +134,10 @@ export const createNotificationTemplateWithTranslations = async ( ({ ...t, template_id: template.id, - created_time: Date.now(), }) as NotificationTemplateTranslation, ) - await createNotificationTemplateTranslations(fullTranslations, pg) + await createNotificationTemplateTranslations(fullTranslations) } return template.id @@ -149,15 +150,19 @@ export const createNotificationTemplateWithTranslations = async ( export const createUserNotifications = async ( templateId: string, userIds: string[], - pg: SupabaseDirectClient, + templateData?: {[key: string]: string | number | boolean}, // Dynamic data for template placeholders // baseNotificationId?: string ) => { const timestamp = Date.now() + const pg = createSupabaseDirectClient() const notifications = userIds.map((userId, index) => ({ user_id: userId, notification_id: `${templateId}-${userId}-${timestamp}-${index}`, template_id: templateId, - data: {isSeen: false}, + data: { + isSeen: false, + templateData: templateData || {}, // Store dynamic template data + }, })) await bulkInsert(pg, 'user_notifications', notifications) @@ -172,17 +177,43 @@ export const createUserNotifications = async ( return notifications.length } +async function getUserIds() { + const pg = createSupabaseDirectClient() + + // Fetch all users + const {data: users, error} = await tryCatch(pg.many>('select id from users')) + + if (error) { + console.error('Error fetching users', error) + return + } + + if (!users || users.length === 0) { + console.error('No users found') + return + } + + const userIds = users.map((u: Row<'users'>) => u.id) + + return userIds +} + /** * Create a bulk notification using the template system * Creates one template and many lightweight user notification entries * Optionally includes translations + * Each template can have dynamic (user-dependent) data */ export const createBulkNotification = async ( template: Omit, - userIds: string[], - pg: SupabaseDirectClient, translations?: Omit[], + templateData?: {[key: string]: string | number | boolean}, // Dynamic data for template placeholders + userIds?: string[], ) => { + if (!userIds) { + userIds = await getUserIds() + if (!userIds) return {} + } const timestamp = Date.now() const templateId = `${template.sourceType}-${timestamp}` @@ -195,22 +226,18 @@ export const createBulkNotification = async ( createdTime: timestamp, }, translations, - pg, ) } else { // Create just the base template - await createNotificationTemplate( - { - ...template, - id: templateId, - createdTime: timestamp, - }, - pg, - ) + await createNotificationTemplate({ + ...template, + id: templateId, + createdTime: timestamp, + }) } - // Create lightweight user notifications - const count = await createUserNotifications(templateId, userIds, pg) + // Create lightweight user notifications with template data + const count = await createUserNotifications(templateId, userIds, templateData) return {templateId, count} } diff --git a/common/messages/de.json b/common/messages/de.json index 28c24a3e..22e6ba05 100644 --- a/common/messages/de.json +++ b/common/messages/de.json @@ -474,6 +474,19 @@ "news.title": "Neuigkeiten", "news.view_on_github": "Auf GitHub ansehen", "notifications.empty": "Sie haben noch keine Benachrichtigungen.", + "notifications.comment.commented": "kommentierte ", + "notifications.comment.on_your_profile": "auf Ihrem Profil", + "notifications.match.proposed_new_match": "schlug einen neuen Match vor:", + "notifications.profile.liked_you": "mag dich gerne!", + "notifications.who_liked_it": "Wer hat es gern?", + "notifications.profile.are_being_shipped_by": "werden versandt von", + "time.units.y": "y", + "time.units.mo": "mo", + "time.units.w": "w", + "time.units.d": "d", + "time.units.h": "h", + "time.units.m": "m", + "time.units.s": "s", "notifications.heading": "Wo möchten Sie benachrichtigt werden, wenn eine Person", "notifications.options.email": "Per E-Mail", "notifications.options.page": "Auf der Benachrichtigungsseite", diff --git a/common/messages/fr.json b/common/messages/fr.json index 2c92f9f4..df087b7e 100644 --- a/common/messages/fr.json +++ b/common/messages/fr.json @@ -474,6 +474,18 @@ "news.title": "Quoi de neuf", "news.view_on_github": "Voir sur GitHub", "notifications.empty": "Vous n'avez pas encore de notifications.", + "notifications.comment.commented": "a commenté ", + "notifications.comment.on_your_profile": "votre profil", + "notifications.match.proposed_new_match": "proposé un nouveau match :", + "notifications.profile.liked_you": "vous a plu !", + "notifications.who_liked_it": "Qui l'a aimé ?", + "time.units.y": "ans", + "time.units.mo": "mo", + "time.units.w": "sem", + "time.units.d": "j", + "time.units.h": "h", + "time.units.m": "m", + "time.units.s": "s", "notifications.heading": "Où voulez-vous être notifié lorsqu'une personne", "notifications.options.email": "Par e‑mail", "notifications.options.page": "Sur la page des notifications", diff --git a/common/src/notifications.ts b/common/src/notifications.ts index 4b732983..5befc04b 100644 --- a/common/src/notifications.ts +++ b/common/src/notifications.ts @@ -1,16 +1,23 @@ -import {Row, SupabaseClient} from 'common/supabase/utils' - // Notification template - stores the shared content for notifications sent to multiple users export type NotificationTemplate = { id: string sourceType: string title?: string - sourceText: string + sourceText: string // May contain placeholders like "{user}" sourceSlug?: string sourceUserAvatarUrl?: string sourceUpdateType?: 'created' | 'updated' | 'deleted' createdTime: number - data?: {[key: string]: any} + data?: {[key: string]: any} // Static data for the template +} + +// Notification template translations +export type NotificationTemplateTranslation = { + templateId: string + locale: string + title?: string + sourceText: string // May contain placeholders like "{user}" + createdTime: number } // User-specific notification data (lightweight - references template) @@ -20,6 +27,8 @@ export type UserNotification = { templateId: string isSeen: boolean viewTime?: number + // Dynamic values to substitute in template placeholders + templateData?: {[key: string]: string | number | boolean} } // Full notification (combines template + user data) - for backwards compatibility @@ -54,37 +63,33 @@ export type Notification = { // New field for template-based notifications templateId?: string -} -// export const NOTIFICATION_TYPES_TO_SELECT = [ -// 'new_match', // new match markets -// 'comment_on_profile', // endorsements -// 'profile_like', -// 'profile_ship', -// ] + // Dynamic values to substitute in template placeholders + templateData?: {[key: string]: string | number | boolean} +} export const NOTIFICATIONS_PER_PAGE = 30 -export async function getNotifications(db: SupabaseClient, userId: string, limit: number) { - const {data} = await db - .from('user_notifications') - .select('*') - .eq('user_id', userId) - .order('data->createdTime', {ascending: false} as any) - .limit(limit) - return data?.map((d: Row<'user_notifications'>) => d) -} - -export async function getUnseenNotifications(db: SupabaseClient, userId: string, limit: number) { - const {data} = await db - .from('user_notifications') - .select('*') - .eq('user_id', userId) - .eq('data->>isSeen', 'false') - .order('data->createdTime', {ascending: false} as any) - .limit(limit) - - return data?.map((d: Row<'user_notifications'>) => d) ?? [] -} - -export type NotificationReason = any // TODO +// export async function getNotifications(db: SupabaseClient, userId: string, limit: number) { +// const {data} = await db +// .from('user_notifications') +// .select('*') +// .eq('user_id', userId) +// .order('data->createdTime', {ascending: false} as any) +// .limit(limit) +// return data?.map((d: Row<'user_notifications'>) => d) +// } +// +// export async function getUnseenNotifications(db: SupabaseClient, userId: string, limit: number) { +// const {data} = await db +// .from('user_notifications') +// .select('*') +// .eq('user_id', userId) +// .eq('data->>isSeen', 'false') +// .order('data->createdTime', {ascending: false} as any) +// .limit(limit) +// +// return data?.map((d: Row<'user_notifications'>) => d) ?? [] +// } +// +// export type NotificationReason = any diff --git a/tests/e2e/utils/seed-test-data.ts b/tests/e2e/utils/seed-test-data.ts index 9b25cb4d..3d75c911 100644 --- a/tests/e2e/utils/seed-test-data.ts +++ b/tests/e2e/utils/seed-test-data.ts @@ -1,4 +1,5 @@ import axios from 'axios' +import {createSomeNotifications} from 'backend/api/src/create-notification' import {tryCatch} from 'common/util/try-catch' import {createSupabaseDirectClient} from 'shared/supabase/init' import {insert} from 'shared/supabase/utils' @@ -48,12 +49,15 @@ async function seedCompatibilityPrompts(pg: any, userId: string | null = null) { console.log('Compatibility prompts created', {data, error}) } +async function seedNotifications() { + await createSomeNotifications() + console.log('Notifications created', {}) +} + type ProfileType = 'basic' | 'medium' | 'full' ;(async () => { const pg = createSupabaseDirectClient() - await seedCompatibilityPrompts(pg) - //Edit the count seedConfig to specify the amount of each profiles to create const seedConfig = [ {count: 1, profileType: 'basic' as ProfileType}, @@ -78,5 +82,8 @@ type ProfileType = 'basic' | 'medium' | 'full' } } + await seedCompatibilityPrompts(pg) + await seedNotifications() + process.exit(0) })() diff --git a/web/components/notification-items.tsx b/web/components/notification-items.tsx index 8ba907d1..f7826edf 100644 --- a/web/components/notification-items.tsx +++ b/web/components/notification-items.tsx @@ -6,6 +6,7 @@ import {sortBy} from 'lodash' import Link from 'next/link' import {ReactNode, useState} from 'react' import {useIsMobile} from 'web/hooks/use-is-mobile' +import {useT} from 'web/lib/locale' import {Col} from './layout/col' import {Row} from './layout/row' @@ -74,7 +75,8 @@ export function CommentOnProfileNotification(props: { }) { const {notification, isChildOfGroup, highlighted, setHighlighted} = props const {sourceUserName, sourceUserUsername, sourceText} = notification - const reasonText = `commented ` + const t = useT() + const reasonText = t('notifications.comment.commented', `commented `) return (
{reasonText} - {!isChildOfGroup && on your profile} + {!isChildOfGroup && ( + {t('notifications.comment.on_your_profile', 'on your profile')} + )}
) @@ -105,6 +109,7 @@ export function NewMatchNotification(props: { }) { const {notification, isChildOfGroup, highlighted, setHighlighted} = props const {sourceContractTitle, sourceText, sourceUserName, sourceUserUsername} = notification + const t = useT() return ( {' '} - proposed a new match: + {t('notifications.match.proposed_new_match', 'proposed a new match:')} + @@ -137,6 +143,7 @@ function ProfileLikeNotification(props: { }) { const {notification, highlighted, setHighlighted, isChildOfGroup} = props const [open, setOpen] = useState(false) + const t = useT() const {sourceUserName, sourceUserUsername} = notification const relatedNotifications: Notification[] = notification.data?.relatedNotifications ?? [ notification, @@ -157,10 +164,11 @@ function ProfileLikeNotification(props: { link={`https://${ENV_CONFIG.domain}/${sourceUserUsername}`} subtitle={<>} > - {reactorsText && } liked you! + {reactorsText && }{' '} + {t('notifications.profile.liked_you', 'liked you!')} @@ -176,6 +184,7 @@ function ProfileShipNotification(props: { }) { const {notification, highlighted, setHighlighted, isChildOfGroup} = props const [open, setOpen] = useState(false) + const t = useT() const {sourceUserName, sourceUserUsername} = notification const relatedNotifications: Notification[] = notification.data?.relatedNotifications ?? [ notification, @@ -198,7 +207,8 @@ function ProfileShipNotification(props: { link={`https://${ENV_CONFIG.domain}/${sourceUserUsername}`} subtitle={<>} > - You and {reactorsText && } are being shipped by{' '} + You and {reactorsText && }{' '} + {t('notifications.profile.are_being_shipped_by', 'are being shipped by')}{' '} diff --git a/web/components/relative-timestamp.tsx b/web/components/relative-timestamp.tsx index db4a67e0..bbc7caad 100644 --- a/web/components/relative-timestamp.tsx +++ b/web/components/relative-timestamp.tsx @@ -2,6 +2,7 @@ import {Placement} from '@floating-ui/react' import dayjs from 'dayjs' import relativeTime from 'dayjs/plugin/relativeTime' import {useIsClient} from 'web/hooks/use-is-client' +import {useLocale, useT} from 'web/lib/locale' import {shortenedFromNow} from 'web/lib/util/shortenedFromNow' import {fromNow} from 'web/lib/util/time' @@ -35,9 +36,11 @@ export function RelativeTimestampNoTooltip(props: { }) { const {time, className, shortened} = props const isClient = useIsClient() + const {locale} = useLocale() + const t = useT() return ( - {isClient && (shortened ? shortenedFromNow(time) : fromNow(time))} + {isClient && (shortened ? shortenedFromNow(time, t) : fromNow(time, false, t, locale))} ) } diff --git a/web/hooks/use-notifications.ts b/web/hooks/use-notifications.ts index f92843c9..f648378e 100644 --- a/web/hooks/use-notifications.ts +++ b/web/hooks/use-notifications.ts @@ -3,6 +3,7 @@ import {type User} from 'common/user' import {first, groupBy, sortBy} from 'lodash' import {useEffect, useMemo} from 'react' import {api} from 'web/lib/api' +import {useLocale} from 'web/lib/locale' import {useApiSubscription} from './use-api-subscription' import {usePersistentLocalState} from './use-persistent-local-state' @@ -69,13 +70,13 @@ function useNotifications(userId: string, count = 15 * NOTIFICATIONS_PER_PAGE) { undefined, 'notifications-' + userId, ) + const {locale} = useLocale() useEffect(() => { if (userId) - api('get-notifications', {limit: count}).then((data) => { + api('get-notifications', {limit: count, locale}).then((data) => { setNotifications(data) }) - }, [userId]) - + }, [userId, locale]) useApiSubscription({ topics: [`user-notifications/${userId}`], onBroadcast: ({data}) => { diff --git a/web/lib/util/shortenedFromNow.ts b/web/lib/util/shortenedFromNow.ts index d61259e5..bd3845f0 100644 --- a/web/lib/util/shortenedFromNow.ts +++ b/web/lib/util/shortenedFromNow.ts @@ -3,10 +3,9 @@ import duration from 'dayjs/plugin/duration' dayjs.extend(duration) -export function shortenedFromNow(time: number): string { +export function shortenedFromNow(time: number, t: any = undefined): string { const diff = dayjs.duration(Math.abs(dayjs().diff(time))) - - return shortenedDuration(diff) + return shortenedDuration(diff, t) } export function simpleFromNow(time: number): string { @@ -15,7 +14,7 @@ export function simpleFromNow(time: number): string { return durationFormat(diff) } -export function shortenedDuration(diff: duration.Duration) { +export function shortenedDuration(diff: duration.Duration, t: any = undefined) { const units: {[key: string]: number} = { y: diff.years(), mo: diff.months(), @@ -27,7 +26,7 @@ export function shortenedDuration(diff: duration.Duration) { for (const unit in units) { if (units[unit] > 0) { - return `${units[unit]}${unit}` + return `${units[unit]}${t(`time.units.${unit}`, unit)}` } } diff --git a/web/pages/notifications.tsx b/web/pages/notifications.tsx index 0959b2f0..0c1580d4 100644 --- a/web/pages/notifications.tsx +++ b/web/pages/notifications.tsx @@ -3,7 +3,6 @@ import {type User} from 'common/src/user' import {Fragment, useEffect, useMemo, useState} from 'react' import {Col} from 'web/components/layout/col' import {UncontrolledTabs} from 'web/components/layout/tabs' -import {EnglishOnlyWarning} from 'web/components/news/english-only-warning' import {NoSEO} from 'web/components/NoSEO' import {NotificationItem} from 'web/components/notification-items' import {NotificationSettings} from 'web/components/notifications' @@ -24,7 +23,6 @@ export default function NotificationsPage() { {t('notifications.title', 'Updates')} -