Finish notif translations

This commit is contained in:
MartinBraquet
2026-02-23 23:49:51 +01:00
parent 3cb5d08801
commit b710fa9f60
14 changed files with 217 additions and 157 deletions

View File

@@ -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(

View File

@@ -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 Playregistered 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<Row<'users'>>('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<Row<'users'>>('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<NotificationTemplateTranslation, 'template_id' | 'created_time'>[] = [
// 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`)
}

View File

@@ -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
}

View File

@@ -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 (

View File

@@ -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<string> => {
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<void> => {
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<NotificationTemplateTranslation, 'template_id' | 'created_time'>[],
pg: SupabaseDirectClient,
): Promise<string> => {
// 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<Row<'users'>>('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<NotificationTemplate, 'id' | 'createdTime'>,
userIds: string[],
pg: SupabaseDirectClient,
translations?: Omit<NotificationTemplateTranslation, 'template_id' | 'created_time'>[],
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}
}

View File

@@ -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",

View File

@@ -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 email",
"notifications.options.page": "Sur la page des notifications",

View File

@@ -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

View File

@@ -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)
})()

View File

@@ -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 (
<NotificationFrame
notification={notification}
@@ -91,7 +93,9 @@ export function CommentOnProfileNotification(props: {
>
<div className="line-clamp-3">
<NotificationUserLink name={sourceUserName} username={sourceUserUsername} /> {reasonText}
{!isChildOfGroup && <span>on your profile</span>}
{!isChildOfGroup && (
<span>{t('notifications.comment.on_your_profile', 'on your profile')}</span>
)}
</div>
</NotificationFrame>
)
@@ -105,6 +109,7 @@ export function NewMatchNotification(props: {
}) {
const {notification, isChildOfGroup, highlighted, setHighlighted} = props
const {sourceContractTitle, sourceText, sourceUserName, sourceUserUsername} = notification
const t = useT()
return (
<NotificationFrame
notification={notification}
@@ -122,7 +127,8 @@ export function NewMatchNotification(props: {
<div className="line-clamp-3">
<NotificationUserLink name={sourceUserName} username={sourceUserUsername} />{' '}
<span>
proposed a new match: <PrimaryNotificationLink text={sourceContractTitle} />
{t('notifications.match.proposed_new_match', 'proposed a new match:')}
<PrimaryNotificationLink text={sourceContractTitle} />
</span>
</div>
</NotificationFrame>
@@ -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 && <PrimaryNotificationLink text={reactorsText} />} liked you!
{reactorsText && <PrimaryNotificationLink text={reactorsText} />}{' '}
{t('notifications.profile.liked_you', 'liked you!')}
<MultiUserReactionModal
similarNotifications={relatedNotifications}
modalLabel={'Who liked it?'}
modalLabel={t('notifications.who_liked_it', 'Who liked it?')}
open={open}
setOpen={setOpen}
/>
@@ -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 && <PrimaryNotificationLink text={reactorsText} />} are being shipped by{' '}
You and {reactorsText && <PrimaryNotificationLink text={reactorsText} />}{' '}
{t('notifications.profile.are_being_shipped_by', 'are being shipped by')}{' '}
<NotificationUserLink
name={creatorName}
username={creatorUsername}
@@ -208,7 +218,7 @@ function ProfileShipNotification(props: {
!
<MultiUserReactionModal
similarNotifications={relatedNotifications}
modalLabel={'Who liked it?'}
modalLabel={t('notifications.who_liked_it', 'Who liked it?')}
open={open}
setOpen={setOpen}
/>

View File

@@ -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 (
<span className={className}>
{isClient && (shortened ? shortenedFromNow(time) : fromNow(time))}
{isClient && (shortened ? shortenedFromNow(time, t) : fromNow(time, false, t, locale))}
</span>
)
}

View File

@@ -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}) => {

View File

@@ -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)}`
}
}

View File

@@ -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() {
<PageBase trackPageView={'notifications page'} className={'mx-4'}>
<NoSEO />
<Title>{t('notifications.title', 'Updates')}</Title>
<EnglishOnlyWarning />
<UncontrolledTabs
name={'notifications-page'}
tabs={[