Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
1import 'vitest-axe/extend-expect';
2import { cleanup } from '@testing-library/react';
3import { afterEach, vi } from 'vitest';
4
5const translations: Record<string, Record<string, string>> = {
6 common: {
7 follow: 'Follow',
8 unfollow: 'Unfollow',
9 signIn: 'Sign in / Register',
10 signOut: 'Sign out',
11 home: 'Sifa ID home',
12 search: 'Search',
13 import: 'Import',
14
15 mainNav: 'Main navigation',
16 footerNav: 'Footer navigation',
17 about: 'About',
18 privacy: 'Privacy',
19 terms: 'Terms',
20 builtBy: 'Built by',
21 poweredByAtproto: 'Powered by the AT Protocol',
22 switchToLight: 'Switch to light mode',
23 switchToDark: 'Switch to dark mode',
24 skipToContent: 'Skip to main content',
25 betaBanner: 'Sifa is alpha software',
26 betaBannerInfoLabel: 'What this means',
27 betaBannerDetail:
28 'This is basically our live dev server. Your profile data is safe in your AT Protocol account, but features and preferences may change before launch.',
29 betaBannerReportLink: 'Report issues on GitHub',
30 dismissBanner: 'Dismiss banner',
31 editProfile: 'Edit profile',
32 viewProfile: 'View profile',
33 openMenu: 'Open menu',
34 closeMenu: 'Close menu',
35 errorTitle: 'Something went wrong',
36 errorDescription: 'An unexpected error occurred. Please try again.',
37 tryAgain: 'Try again',
38 },
39 profile: {
40 experience: 'Experience',
41 education: 'Education',
42 skills: 'Skills',
43 about: 'About',
44 editProfile: 'Edit profile',
45 noProfile: "This profile doesn't exist yet.",
46 addAbout: 'Add a professional summary',
47 editAbout: 'Edit profile summary',
48 readMore: 'Read more',
49 readLess: 'Read less',
50 },
51 import: {
52 title: 'Import from LinkedIn',
53 subtitle:
54 'Bring your professional history to Sifa. Your LinkedIn ZIP is extracted in your browser and never leaves your device.',
55 uploadStep: 'Upload',
56 previewStep: 'Review',
57 confirmStep: 'Import',
58 },
59 'import.upload': {
60 heading: 'Upload your LinkedIn data export',
61 descriptionPrefix: 'Go to ',
62 descriptionLinkText: 'LinkedIn\'s "Download my data" page',
63 descriptionMiddle:
64 ' and select "Download larger data archive" (the first option on that page). Upload the ZIP file you receive (batch 1 arrives in your e-mail inbox in ~10 minutes).',
65 screenshotAlt:
66 'LinkedIn\'s Download my data page showing the "Download larger data archive" option selected',
67 screenshotCaption: 'What it looks like on LinkedIn',
68 dropZone: 'Drag and drop your LinkedIn ZIP file here, or click to browse',
69 dropZoneLabel: 'Drop zone for LinkedIn ZIP file',
70 processing: 'Processing ZIP file...',
71 fileTypeError:
72 'Please select a ZIP file (.zip). LinkedIn exports are delivered as ZIP archives.',
73 fileSizeError: 'File is too large (max 500 MB). Try re-downloading your LinkedIn export.',
74 privacyNote:
75 'Your LinkedIn ZIP is extracted in your browser and never leaves your device. The structured profile data is then written directly to your Personal Data Server through our API.',
76 publicDataNotice:
77 'Everything you import will be publicly visible to anyone, including people without a Sifa account. Unlike LinkedIn (where some data is hidden from logged-out visitors), Sifa profiles are fully public. Review your data before importing.',
78 },
79 'import.preview': {
80 heading: 'Review imported data',
81 duplicatesTitle: 'Some items already exist on your profile',
82 duplicatesBody: '{count} items match your existing profile data and will be overwritten.',
83 newItemsNote: '{count} items are new.',
84 removeNote: "You can remove items you don't want to import.",
85 existingTitle: 'Your profile already has data',
86 existingBody:
87 'Importing will replace all existing profile data with the data below. Your profile headline and summary will also be updated.',
88 alreadyOnProfile: 'Already on profile',
89 new: 'New',
90 itemCount: '{count} items to import',
91 confirmButton: 'Confirm & Import',
92 back: 'Back',
93 profile: 'Profile',
94 noPositions: 'No positions found in export.',
95 noEducation: 'No education found in export.',
96 noSkills: 'No skills found in export.',
97 },
98 'import.confirm': {
99 importing: 'Importing your data...',
100 writingRecords: 'Writing {count} records to your Personal Data Server...',
101 success: 'Import complete',
102 partial: 'Import partially complete',
103 error: 'Import failed',
104 successMessage: 'Successfully imported to your profile:',
105 warningPrefix: 'Import completed with warnings:',
106 viewFailed: 'View failed items',
107 retryFailed: 'Retry failed items',
108 retry: 'Retry',
109 viewProfile: 'View your profile',
110 goToProfile: 'Go to profile',
111 },
112 search: {
113 placeholder: 'Search people by name, skills, or headline',
114 noResults: 'No results found for "{query}"',
115 },
116 sections: {
117 career: 'Career',
118 education: 'Education',
119 educationEntry: 'Education',
120 courses: 'Courses',
121 skills: 'Skills',
122 projects: 'Projects',
123 credentials: 'Credentials',
124 publications: 'Publications',
125 volunteering: 'Volunteering',
126 awards: 'Awards',
127 languages: 'Languages',
128 otherProfiles: 'Links',
129 alsoFindOn: 'Also find {name} on…',
130 usedIn: 'Used in',
131 verified: 'Verified',
132 verifyHintGithub:
133 'Add your Sifa profile URL in the "URL" field or under "Social accounts" in your GitHub profile settings. We\'ll verify the link automatically when you save.',
134 verifyHintWebsite:
135 "Add the following tag to your site's head section. We'll verify the link automatically when you save.",
136 verifyHintFediverse:
137 "Add your Sifa profile URL to one of your Fediverse profile metadata fields. We'll verify the link automatically when you save.",
138 verifyHintRss:
139 'Add a link with rel="me" pointing to your Sifa profile URL on your feed\'s HTML page. We\'ll verify the link automatically when you save.',
140 verificationMethods: 'Verification methods',
141 verifiedViaKeytrace: 'Verified via Keytrace',
142 verifiedViaRelMe: 'Verified via rel=me link',
143 verifiedViaDns: 'Verified via DNS record',
144 verifiedViaMeta: 'Verified via meta tag',
145 hideLink: 'Hide from profile',
146 unhideLink: 'Show on profile',
147 keytraceManaged: 'Managed via Keytrace',
148 },
149 editor: {
150 save: 'Save',
151 saving: 'Saving...',
152 cancel: 'Cancel',
153 close: 'Close',
154 add: 'Add',
155 failedToSave: 'Failed to save',
156 added: '{section} added',
157 updated: '{section} updated',
158 removed: '{section} removed',
159 failedToDelete: 'Failed to delete',
160 copyUrl: 'Copy profile URL',
161 copySnippet: 'Copy snippet',
162 copied: 'Copied!',
163 },
164 trackRecord: {
165 title: 'Track Record',
166 endorsementsTitle: 'Endorsements',
167 endorsementsDesc: 'Skills endorsed by other professionals.',
168 verifiedTitle: 'Verified Accounts',
169 verifiedDesc: 'Verified platform accounts.',
170 reactionsTitle: 'Reactions Received',
171 reactionsDesc: 'Reactions on your posts.',
172 communityTitle: 'Community Presence',
173 communityDesc: 'Activity in communities.',
174 mutualTitle: 'Mutual Connections',
175 mutualDesc: 'Connections you share.',
176 sharedTitle: 'Shared History',
177 sharedDesc: 'Common employers or education.',
178 },
179 activityOverview: {
180 title: 'Activity',
181 viewAll: 'View full activity',
182 comingSoon: 'Activity overview coming soon.',
183 },
184 dataTransparency: {
185 title: 'Your data on the AT Protocol',
186 body: 'Your professional profile is stored in your Personal Data Server (PDS). This data is public and portable -- you can inspect exactly what is stored and take it to any compatible provider.',
187 viewRawData: 'View your raw data',
188 },
189 identityCard: {
190 label: 'Professional identity',
191 avatarAlt: "{name}'s profile photo",
192 verified: 'Verified',
193 statConnections: 'Connections',
194 statEndorsements: 'Endorsements',
195 statReactions: 'Reactions',
196 trustStatsLabel: 'Trust stats',
197 editProfile: 'Edit profile',
198 shareProfile: 'Share profile',
199 share: 'Share',
200 linkCopied: 'Link copied',
201 embed: 'Embed',
202 viewOnSifa: 'View on Sifa ID',
203 followers: '{count} followers',
204 followersOnBluesky: '{count} followers on Bluesky',
205 },
206 home: {
207 title: 'Sifa ID',
208 subtitle: 'Professional identity on the AT Protocol. Own your career narrative.',
209 comingSoon: 'Timeline and feed are coming soon.',
210 searchProfiles: 'Search profiles',
211 importLinkedIn: 'Import from LinkedIn',
212 },
213 activity: {
214 title: 'Activity for {handle}',
215 comingSoon: 'Activity stream is coming soon.',
216 introVisitor: 'Activity from across the AT Protocol ecosystem.',
217 introDismiss: 'Dismiss',
218 },
219 activityCard: {
220 fallback: 'Activity on {appName}',
221 },
222 about: {
223 title: 'About Sifa ID',
224 mission: 'Sifa is a decentralized professional identity network built on the AT Protocol.',
225 atproto: 'Built on the AT Protocol.',
226 openSource: 'Professional profile lexicons and import tools are open source.',
227 builtBy: 'Built by',
228 },
229 privacy: {
230 title: 'Privacy Policy',
231 lastUpdated: 'Last updated: March 2026',
232 dataStorageTitle: 'Your data, your PDS',
233 dataStorageBody: 'Your professional profile data is stored in your PDS.',
234 importTitle: 'LinkedIn import',
235 importBody: 'ZIP is extracted in your browser and never leaves your device.',
236 cookiesTitle: 'Cookies and sessions',
237 cookiesBody: 'Session cookies only.',
238 contactTitle: 'Contact',
239 contactBody: 'Contact us at privacy@sifa.id.',
240 },
241 embedBuilder: {
242 pageTitle: 'Embed a Sifa Profile Card',
243 pageDescription:
244 'Add a live professional profile card to any website with a single line of code.',
245 pageSubtitle: 'Add a live professional profile card to any website with a single line of code.',
246 identifierLabel: 'Handle or DID',
247 codeLabel: 'Embed code',
248 themeNote:
249 "The embed automatically adjusts between light and dark mode based on the visitor's browser setting.",
250 previewLabel: 'Preview',
251 previewTitle: 'Embed preview',
252 copy: 'Copy',
253 copied: 'Copied to clipboard',
254 enterHandle: 'Enter a handle or DID to see a preview',
255 },
256 heatmap: {
257 loading: 'Loading activity...',
258 loadingSlow: "Dang, that's a lot of activity! Hold on...",
259 less: 'Less',
260 more: 'More',
261 noActivity: 'No activity',
262 totalActions: '{count} total',
263 actionsInPeriod: '{count} actions in {months} months',
264 mostActive: 'Most active: {app}',
265 appsActive: '{count} apps active',
266 emptyState: 'Activity across the ATmosphere will appear here.',
267 emptyStateOwner: 'Activity across the ATmosphere will appear here as you participate.',
268 showFullYear: 'Show full year',
269 showSixMonths: 'Show 6 months',
270 filterDate: 'Showing {date}',
271 clearFilter: 'Clear date',
272 },
273 endorsement: {
274 endorseSkill: 'Endorse {name}',
275 endorsementContext: "How do you know this person's expertise?",
276 workedTogether: 'Worked together at...',
277 collaboratedIn: 'Collaborated in...',
278 supervisedBy: 'Supervised / was supervised by',
279 coAuthored: 'Co-authored',
280 otherContext: 'Other',
281 endorseComment: "Share context about this person's expertise (optional)",
282 endorseSubmit: 'Endorse',
283 endorseCancel: 'Cancel',
284 endorseAttribution: 'Your endorsement will be attributed to your profile',
285 contextDetail: 'Details (optional)',
286 endorsedBy: 'Endorsed by',
287 relationshipWorkedTogether: 'Worked together at {detail}',
288 relationshipCollaboratedIn: 'Collaborated in {detail}',
289 relationshipSupervisedBy: 'Supervised / was supervised by',
290 relationshipCoAuthored: 'Co-authored',
291 relationshipOther: '{detail}',
292 },
293 terms: {
294 title: 'Terms of Service',
295 lastUpdated: 'Last updated: March 2026',
296 serviceTitle: 'The service',
297 serviceBody: 'Sifa provides a web interface for professional profiles.',
298 accountsTitle: 'Accounts',
299 accountsBody: 'You sign in with your AT Protocol account.',
300 contentTitle: 'Content',
301 contentBody: 'You are responsible for your profile information.',
302 disclaimerTitle: 'Disclaimer',
303 disclaimerBody: 'Sifa is provided as-is during beta.',
304 },
305};
306
307function createTranslator(namespace?: string) {
308 return (key: string, params?: Record<string, string>) => {
309 const ns = namespace ? translations[namespace] : undefined;
310 let value = ns?.[key] ?? key;
311 if (params) {
312 for (const [k, v] of Object.entries(params)) {
313 value = value.replace(`{${k}}`, v);
314 }
315 }
316 return value;
317 };
318}
319
320// Mock next-intl for client components
321vi.mock('next-intl', () => ({
322 useTranslations: (namespace?: string) => createTranslator(namespace),
323}));
324
325// Mock next-intl/server for server components
326vi.mock('next-intl/server', () => ({
327 getTranslations: async (namespace?: string) => createTranslator(namespace),
328}));
329
330// Mock auth
331vi.mock('@/lib/auth', () => ({
332 getSession: vi.fn().mockResolvedValue({ status: 'unauthenticated' }),
333 getLoginUrl: () => '/api/auth/login',
334 getLogoutUrl: () => '/api/auth/logout',
335}));
336
337// Mock next/navigation
338vi.mock('next/navigation', () => ({
339 useRouter: () => ({
340 push: vi.fn(),
341 replace: vi.fn(),
342 back: vi.fn(),
343 forward: vi.fn(),
344 refresh: vi.fn(),
345 prefetch: vi.fn(),
346 }),
347 useSearchParams: () => new URLSearchParams(),
348 usePathname: () => '/',
349}));
350
351// Mock next-themes
352vi.mock('next-themes', () => ({
353 useTheme: () => ({
354 theme: 'light',
355 setTheme: vi.fn(),
356 resolvedTheme: 'light',
357 }),
358 ThemeProvider: ({ children }: { children: React.ReactNode }) => children,
359}));
360
361afterEach(() => {
362 cleanup();
363});