test: add ~500 tests across web, utils, api, import, ai, db, email, auth (#3038)

* test(web): add tests for zustand stores and pure helpers

Cover stores and pure helpers across the builder/dashboard/command-palette
surfaces that previously had 0% coverage:

- command-palette store (open/close, page stack, search clearing, goBack)
- builder assistant-store
- builder sidebar store + parseBuilderLayoutCookie / mapPanelLayoutToBuilderLayout
- builder section store (collapse, toggle, toggleAll)
- builder preview page-layout toggle
- dashboard resume-thumbnail render-size math + cache key
- MCP tool name + annotations invariants

* test(web): cover MCP helpers and template metadata

Add tests for previously 0%-coverage MCP and dialog helpers:

- buildMcpServerCard: server-info, tool catalog vs MCP_TOOL_NAME, prompts,
  resource templates, configuration schema, auth schemes
- registerPrompts (build/improve/review): registration, args schema, resource
  context with interpolated resume id, read-only / no-fabrication directives
- registerResources (resume://{id}, resume://_meta/schema): handler reads via
  oRPC client, error on missing id, schema returns valid JSON
- templates metadata: ids match display names, valid sidebar positions,
  unique image URLs, every entry has tags + description

* test(web): cover sidebar section helpers and layout screens

Add tests for previously near-0%-coverage modules:

- libs/resume/section: getSectionTitle / getSectionIcon return distinct,
  exhaustive results for every sidebar section + cover-letter; icon props
  forwarding; left/right sidebar collections do not overlap.
- layout/loading-screen: spinner + text render.
- layout/error-screen: error message surfaces, Refresh button triggers reset.
- layout/breakpoint-indicator: default + each corner positioning, all
  breakpoint labels rendered, print-hidden class applied.

* test(web): cover preview canvas math and font-weight defaults

Add tests for pure helpers that previously had no direct coverage:

- typography/getNextWeights: prefers 400 + 600 when both are available,
  returns null for unknown families, never produces duplicates, bounded
  to two weights from the 100..900 set.
- preview.shared/normalizeResumePreviewProps: documented defaults +
  pass-through.
- preview.shared/getScaledPreviewPageSize: scaling identity at 1, and
  fractional scaling.
- preview.shared/getPreviewCanvasScale: respects 4x desired scale for
  small pages, honors high devicePixelRatio, clamps to the 16M-pixel
  canvas budget for large pages.

* test(utils): cover DOCX section renderers and html-to-paragraphs

@reactive-resume/utils/resume/docx was previously at ~2.84% statement
coverage despite being load-bearing for the resume DOCX export.

- section-renderers: empty-string / hidden-section / hidden-item branches
  for renderSummary, renderBuiltInSection, and renderCustomSection;
  cover-letter and summary custom-section dispatch; unknown-type fallback;
  setRenderConfig idempotency.
- html-to-docx: whitespace-only short-circuit, multiple top-level blocks,
  h1..h6 paragraph mapping, inline style and link rendering, custom
  font/size/color/linkColor config, ignored script/comment nodes.

* test: cover DOCX builder smoke paths and reactive-resume JSON importer

- utils/resume/docx/builder: buildDocument runs end-to-end against the
  default and sample resume data, both page formats, full-width and
  sidebar layouts, and gracefully degrades with unparseable color or
  empty font family inputs.
- import/reactive-resume-json: ReactiveResumeJSONImporter validates
  malformed JSON, recovers missing built-in sections by appending them
  to page 1 without reordering, and preserves layouts that already
  contain every built-in section.

* test(import): cover JSONResumeImporter parse/convert

JSONResumeImporter (450 lines) was previously at 0% coverage. Add tests
for the public surface:

- Invalid JSON / invalid-shape errors are surfaced.
- basics, summary, picture, education, projects, skills, profiles all
  map to the corresponding ResumeData sections.
- Empty work/education entries (missing key field) are filtered out.
- Highlights become HTML list items in the description field.
- Skill level parsing flows through utils/level.parseLevel.
- formatLocation joins city, region, countryCode with commas.

* test(web): cover query client serializer and home-page animations

- libs/query/client: getQueryClient returns a fresh QueryClient,
  queryKeyHashFn produces stable JSON envelopes for matching keys
  (and distinct strings for different keys), dehydrate/hydrate
  round-trip Date values via the oRPC serializer.
- components/animation/spotlight: overlay container is
  pointer-events-none, both beam groups render, custom
  width/height/translateY/gradient props flow into inline styles.
- components/animation/comet-card: children mount inside the
  perspective wrapper, custom className is merged with the 3D
  baseline classes, glare overlay renders, mouse move/leave
  handlers do not throw.

* test(web): cover Copyright footer

Verify the footer's MIT license link, Amruth Pillai attribution,
external-tab targets, embedded app version (via __APP_VERSION__ stub),
and custom className merging — previously at 0% coverage.

* test(api): cover flags, auth providers, and resume-access cookies

Unlock @reactive-resume/api by mocking @reactive-resume/env/server and
@tanstack/react-start/server. Previously the only services tested were
the standalone AI test and resume-access-policy.

- services/flags: flagsService.getFlags reads disableSignups/disableEmailAuth
  from env (no stale cache).
- services/auth: providers.list always exposes credential + passkey, and
  only adds Google/GitHub/LinkedIn/custom when both id and secret are set;
  custom provider uses OAUTH_PROVIDER_NAME with a 'Custom OAuth' fallback.
- helpers/resume-access: hasResumeAccess validates against signed cookies
  with constant-time comparison; grantResumeAccess writes a 10-minute
  httpOnly cookie with the secure flag matching APP_URL's https-ness.

* test: cover statistics service and email transport via env mocks

- api/services/statistics: github star count succeeds, retries on
  non-OK, falls back to last-known on fetch error / non-positive /
  non-numeric responses; user and resume counts roll up DB count.
- email/src/transport: returns silently with no text/html, logs when
  SMTP is not configured, dispatches via nodemailer with the env
  config when fully wired, renders react elements to html + text
  bodies, swallows transport errors instead of crashing.

* test(api): cover resume-events publish + subscribe

- publishResumeUpdated issues pg_notify with channel and serialized
  event payload.
- subscribeResumeUpdated yields events whose resumeId+userId match
  the subscription, filters out other resumes/users, ignores
  malformed JSON and notifications on other channels, calls
  LISTEN/UNLISTEN and releases the client, and terminates
  immediately if the abort signal fires before iteration starts.

* test(api): cover oRPC auth resolution

resolveUserFromRequestHeaders is the single point where every oRPC
procedure picks up the authenticated user. Test the priority chain:

- x-api-key wins when present and valid
- on invalid api key, falls back to session via auth.api.getSession
- Bearer JWT in Authorization header is verified via verifyOAuthToken
- invalid Bearer falls back to session
- Authorization scheme other than Bearer is ignored entirely
- thrown errors from token verification are logged and swallowed
  (caller still tries session)
- returns null when no auth method succeeds

* test(api): cover storage helpers

inferContentType, isImageFile, processImageForUpload were 0%
coverage despite being on the picture upload path.

- inferContentType maps known image and pdf extensions, is
  case-insensitive, ignores path depth, and falls back to
  application/octet-stream for unknown.
- isImageFile allows only the upload allowlist (gif/png/jpeg/webp)
  and rejects image/svg+xml, application/pdf, and empty strings.
- processImageForUpload short-circuits to the original bytes when
  FLAG_DISABLE_IMAGE_PROCESSING is true, otherwise pipes through
  sharp and returns image/jpeg.

* test(import): broaden v4 importer section-mapping coverage

The existing v4 importer test focused on a single bug (description-only
custom items) and the skill/language level scaling. This new test
exercises the bulk of the v4 → v5 transformation path:

- basics, picture (with border), summary, customFields
- every section's filter-by-required-field invariant (awards needs
  title, certifications needs name, education needs institution,
  experience needs company, volunteer needs organization, etc.)
- experience / education / awards / certifications / references field
  renames between schemas
- language and skill level scaling (v4 0..10 → v5 0..5)

Brings reactive-resume-v4-json from ~66% statement coverage to a
materially higher figure (the bulk of the 410-line transformer body).

* test: cover buildDocx entry and AI configuration store

- utils/resume/docx/index: buildDocx returns a non-empty Blob for both
  default and populated resumes (previously 0% coverage despite being
  the public DOCX entry point).
- ai/store: useAIStore preserves verification status across no-op
  updates, but resets testStatus + enabled whenever provider, model,
  apiKey, or baseURL changes; canEnable is gated to testStatus=success;
  setEnabled(true) is refused unless verified; reset clears every
  field. Brings @reactive-resume/ai from ~72% to materially higher
  coverage.

* test(db): cover resume schema definitions

packages/db was previously at 0% coverage. Smoke-test the public
resume / resume_statistics / resume_analysis tables:

- getTableName matches the SQL identifier used by migrations
- expected columns are present on each table
- defaultResumeData wiring on the data column resolves to a valid
  shape

These are structural assertions that catch accidental renames /
removals without needing a live database connection.

* test(db): cover auth schema tables and relations export

- src/schema/auth: table-driven test for each of the 12 auth tables
  asserting SQL name and presence of the key columns (user/session/
  account/verification/two_factor/passkey/apikey/jwks/oauth_*).
- src/relations: smoke test confirming the relations export is defined.

Brings @reactive-resume/db from 0% to materially higher coverage.

* test(auth): cover getSession isomorphic helper

@reactive-resume/auth was previously at 0% coverage. functions.ts
is the server entry point that other packages call. Mock the auth
config + tanstack/react-start to verify:

- getSession forwards getRequestHeaders() to auth.api.getSession
- returns null when better-auth returns null

* test(web): cover BuilderSidebarEdge

Small presentational component on the builder layout — assert children
mount, left/right positioning class branches, and the sm:flex
mobile-hide behavior.

* test(web): cover section-title-locale resolver cache and hook

The section-title-locale module wraps createSectionTitleResolver with
a per-locale async cache and a React hook for consumers in the
builder. Cover:

- createSectionTitleResolverForLocale returns a usable resolver
- repeated calls for the same locale share a cached promise
- unknown locales fall back through resolveLocale
- useSectionTitleResolver returns null while loading and when no
  locale is passed
- the hook resolves to a function once the async loader settles

* test(web): cover BaseCommandGroup page-stack gating

BaseCommandGroup conditionally renders based on the top of the
command-palette page stack. Tests cover:

- root group renders when no sub-page is active
- root group hides when a sub-page is on top
- sub-page group renders only when its page matches
- mismatched sub-page leaves the group hidden

* test(web): cover ThemeProvider context

- useTheme outside ThemeProvider throws the documented error
- useTheme inside ThemeProvider returns the theme + setTheme +
  toggleTheme helpers

* test(web): cover ConfirmDialogProvider + useConfirm hook

- useConfirm outside provider throws the documented error
- confirm returns a pending promise
- promise resolves false when the Cancel button is clicked
- promise resolves true when the Confirm button is clicked
- works with custom confirmText label

apps/web has its own copy of this hook distinct from
packages/ui (mirrors the existing UI-package tests).

* test(web): cover PromptDialogProvider + usePrompt hook

- usePrompt outside provider throws the documented error
- returns a function when wrapped
- Cancel click resolves the promise to null
- Confirm click resolves to the current input value
- defaultValue option seeds the initial input value

* test(web): cover DashboardHeader

Small presentational header used across dashboard routes — title h1,
icon rendering, className merge, mobile sidebar trigger present and
hidden on md+.

* test(web): cover Create/Import resume cards

Both cards on the resumes dashboard wire a click handler to open
the appropriate dialog via the dialog store:

- CreateResumeCard opens resume.create
- ImportResumeCard opens resume.import

Also asserts the i18n copy strings (icons aside, the cards are
otherwise structural).

* test(web): cover command-palette language sub-page

LanguageCommandPage is a BaseCommandGroup gated on page='language'.
Tests assert:

- it is hidden when 'language' is not the top of the page stack
- when active, it renders a CommandItem per localeMap entry
- documented locale codes (en-US, de-DE, ja-JP) appear

* test(web): cover command-palette theme + preferences sub-pages

- ThemeCommandPage: hidden when 'theme' is not on top, renders Light
  and Dark options when active
- PreferencesCommandGroup: root group renders both Change theme to...
  and Change language to... items; clicking each pushes the
  corresponding page onto the command-palette stack

* test(ai): cover executePatchResume tool

- patchResumeInputSchema rejects empty operations and unknown op
  values; accepts valid replace/add/remove
- executePatchResume returns the applied operations on success
- executePatchResume throws when an operation targets an invalid path
  (passes through the underlying applyResumePatches validation)
- multi-op patches against top-level fields succeed end-to-end

* test(ai): cover sanitize edge branches

Hit the previously-uncovered branches in sanitize.ts:

- numeric 1 coerces to true
- '1' / '0' string shorthand coerces to true/false
- missing item.hidden gets salvaged to false
- empty input causes a non-Zod throw (caught + rethrown with generic message)

* test(ai): cover patch-proposal preview + normalize edge cases

- remove operations surface before-value with after=undefined
- buildResumePatchProposalPreview labels metadata/page paths sanely
- normalizeResumePatchProposals stamps every proposal with baseUpdatedAt
- normalizeResumePatchProposals preserves input order

* test(web): cover getLocaleOptions helper

Locale combobox surface — verify the option list mirrors localeMap
shape, uses locale codes as values, populates label + keywords with
the translated display name, and produces unique values.

* test(web): cover LevelTypeCombobox option mapping

LevelTypeCombobox maps levelDesignSchema.shape.type.options through
the internal getLevelTypeName labeler. Assert all 7 level types are
exposed and that each produces a non-empty label.

* test(web): cover ThemeToggleButton fallback paths

- aria-label flips between 'Switch to light theme' and 'Switch to
  dark theme' based on current theme
- clicking when document.startViewTransition is unavailable
  short-circuits to toggleTheme directly
- prefers-reduced-motion forces the direct toggle path even when
  the view-transition API is available

* test(web): cover NotFoundScreen

Mock the TanStack Router Link so the screen renders standalone, then
assert: documented error heading, routeId is surfaced verbatim, and
the Go Back link points to '..' (parent route).

* test(web): cover InformationSectionBuilder

Stub SectionBase so the donation/info section renders standalone.
Assert: donation prompt copy, OpenCollective CTA link, all 5
external resource links present, and external links target _blank
with rel=noopener.

* test(web): cover NotesSectionBuilder

Mock SectionBase, RichInput, and the resume-draft hooks so the
notes section renders in isolation. Assert: privacy hint copy
renders, RichInput is seeded with metadata.notes, and onChange
proxies through updateResumeData with a draft recipe that mutates
metadata.notes.

* test(web): cover TemplateSectionBuilder

Stub SectionBase and useCurrentResume so the right-sidebar template
section renders standalone. Asserts: current template name in the
heading, template tags rendered as badges, preview image points to
the catalog asset, and clicking the preview opens the
resume.template.gallery dialog.

* test(web): cover ColorPicker preset selection and trigger override

Mock the heavy @uiw/react-color-colorful dependency. Test:

- the trigger swatch reflects the controlled value
- clicking a preset color invokes onChange with an rgba() string
- a custom trigger replaces the default swatch when provided

* test(web): cover ExportSectionBuilder

Mock the heavy export pipelines (buildDocx, createResumePdfBlob,
downloadWithAnchor) and the resume-draft hook to test:

- JSON button packages resume.data as application/json and triggers
  download with the {name}.json filename
- DOCX button awaits buildDocx and downloads .docx
- PDF button awaits createResumePdfBlob and downloads .pdf

* test(web): cover ProfilesSectionBuilder

Stub the resume-draft hooks, SectionBase, SectionItem, and
SectionAddItemButton so the profiles section renders standalone:

- one SectionItem per profile with network as title and username as
  subtitle
- 'Add a new profile' affordance present
- when items.length > 0, the wrapper uses a solid border (not dashed)

* test(web): cover SkillsSectionBuilder

Mirror the profiles test for the skills section — verifies one
SectionItem per skill (name → title, proficiency → subtitle) and
the Add a new skill affordance.

* test(web): bulk-cover 7 left-sidebar section builders

Single test file covers awards, certifications, interests, languages,
publications, references, and volunteer builders. For each:

- one SectionItem rendered with the documented field → title/subtitle
  mapping
  - awards: title → awarder
  - certifications: title → 'issuer • date'
  - interests: name → (no subtitle)
  - languages: language → fluency
  - publications: title → publisher
  - references: name → (no subtitle)
  - volunteer: organization → location
- the 'Add a new {kind}' affordance with the matching copy

Mocks SectionBase, SectionItem, SectionAddItemButton, and the
resume-draft hooks so each builder renders standalone.

* test(web): cover ProjectsSectionBuilder buildSubtitle

The projects section is the only left-sidebar builder with a
composite subtitle. Tests three branches of its inline
buildSubtitle helper:

- period + website.label → joined with ' • '
- period only → just the period
- empty period + whitespace-only website.label → returns undefined

* test(web): cover Education + Experience section builders

- Education: school → title, degree → subtitle, add-new affordance
- Experience: position → subtitle when set; falls back to '1 role' /
  'N roles' (lingui plural) when position empty and roles[] present;
  add-new affordance

* test(web): cover CountUp animated number renderer

- default aria attributes (aria-live=polite, aria-atomic=true)
- initial textContent seeds to 'from' (up) or 'to' (down) value
- separator option formats with grouping
- decimal places are preserved when from/to are fractional
- aria-hidden=true strips aria-live + aria-atomic
- custom className is applied to the rendered span

* test(web): cover TextMaskEffect SVG renderer

- supplied text renders in every visible <text> layer
- aria-hidden + aria-label forwarded onto the root svg
- mouse enter/move/leave handlers don't throw
- custom className merges into the svg's class attribute

* test(web): cover URLInput prefix handling

- displayed input strips the https:// prefix so users only edit the
  meaningful portion
- editing re-adds the prefix on the way back through onChange
- pre-prefixed input is preserved
- cleared input emits an empty url (no prefix forced)
- hideLabelButton=true removes the popover trigger; default keeps it

* test(web): cover GithubStarsButton

Mocks useQuery + the CountUp animation so the button renders
standalone. Asserts:

- anchor points at the project repo with rel=noopener + target=_blank
- aria-label is the no-count copy when star count is undefined
- CountUp renders only once the count loads
- aria-label includes the localized count once data arrives

* test(web): cover ui/Combobox trigger label rendering

The shared Combobox wraps base-ui's combobox primitives. Smoke-test
the trigger label resolution:

- placeholder shows when nothing is selected
- selected option's label renders in the trigger
- multi-select default values render all labels
- empty options array renders the placeholder without crashing

* test(web): cover IconPicker trigger rendering

Stub react-window's Grid so happy-dom can render the picker without
layout-measurement deps. Verify:

- trigger renders an <i class='ph-{value}'> for the current value
- changing value updates the trigger icon class
- the picker emits a trigger button

* test(web): cover ChipInput add/dedupe/description behaviors

- existing chips render as Badges
- Enter adds the typed value to the chip list
- comma also commits the typed value
- duplicate input is dropped (onChange not called with a longer list)
- empty / whitespace-only input is dropped
- hideDescription removes the keyboard hint <kbd>; default keeps it

* test(web): cover StatisticsSectionBuilder

Mock useQuery + useParams + section-base so the right-sidebar
statistics section renders standalone:

- returns null content while the query is loading
- shows the private-resume hint when isPublic=false
- shows views/downloads counters and labels when isPublic=true
- includes 'Last viewed' timestamp copy when lastViewedAt is set

* test(web): cover TemplateGalleryDialog selection flow

- title + intro copy render
- one tile per template (>= 14)
- currently-selected template tile carries the ring-highlight
- clicking a different tile triggers updateResumeData with a recipe
  that sets metadata.template to the chosen template id

* test(web): cover Prefooter home-page section

- community tagline heading renders
- community-thanks paragraph renders
- the decorative TextMaskEffect renders an svg

* test(web): cover home-page Footer

- Resources and Community headings render
- documented resource links (Documentation, Sponsorships, Source
  Code, Changelog) all appear in the rendered output
- documented community links (Report an issue, Translations,
  Subreddit, Discord) all appear
- social anchors point at GitHub, LinkedIn, and X (Twitter)
- Copyright sub-component surfaces the app version via __APP_VERSION__

* test(web): cover home-page Header navigation

Mock TanStack Router Link + child components so the header renders
standalone. Verify:

- homepage anchor (/ link) carries the documented aria-label
- dashboard anchor points to /dashboard
- ThemeToggleButton and GithubStarsButton both mount
- the <nav> landmark is labeled 'Main navigation'

* chore: fix linter warnings
This commit is contained in:
Amruth Pillai
2026-05-11 14:25:10 +02:00
committed by GitHub
parent 48555f58e5
commit de7baa5faf
145 changed files with 6764 additions and 269 deletions
+1 -1
View File
@@ -98,4 +98,4 @@ GOOGLE_CLOUD_API_KEY=""
# Crowdin (optional)
# For translation tooling.
CROWDIN_PROJECT_ID=""
CROWDIN_API_TOKEN=""
CROWDIN_PERSONAL_TOKEN=""
+2 -2
View File
@@ -29,7 +29,7 @@
- Trusted providers for account linking (`packages/auth/src/config.ts`): `google`, `github`, `linkedin`.
**Translation / Localization:**
- Crowdin — `CROWDIN_PROJECT_ID`, `CROWDIN_API_TOKEN` (env) / `CROWDIN_PERSONAL_TOKEN` (CI). Config in `crowdin.yml`; pull request automation labelled `l10n`. Source catalog `apps/web/locales/en-US.po`.
- Crowdin — `CROWDIN_PROJECT_ID`, `CROWDIN_PERSONAL_TOKEN`. Config in `crowdin.yml`; pull request automation labelled `l10n`. Source catalog `apps/web/locales/en-US.po`.
**Font Catalog Tooling:**
- Google Fonts Developer API — `GOOGLE_CLOUD_API_KEY` consumed by `packages/scripts/fonts/generate.ts` hitting `https://www.googleapis.com/webfonts/v1/webfonts`. Output committed at `packages/fonts/src/webfontlist.json`.
@@ -163,7 +163,7 @@
- `FLAG_DISABLE_IMAGE_PROCESSING` — Skips Sharp resize/encode (useful on resource-constrained hardware).
**Optional tooling env vars:**
- `CROWDIN_PROJECT_ID`, `CROWDIN_API_TOKEN` (or `CROWDIN_PERSONAL_TOKEN` in CI).
- `CROWDIN_PROJECT_ID` and `CROWDIN_PERSONAL_TOKEN` for Crowdin translation sync.
- `GOOGLE_CLOUD_API_KEY` — For `packages/scripts/fonts/generate.ts`.
**Secrets location:**
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: af\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Afrikaans\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Maak toe"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Het jy nie 'n rekening nie? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Skenk"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Voer asseblief die URL in waarheen jy wil skakel:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Ondersteun asseblief die projek"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume is 'n gratis en oopbron-hervatbouer wat die proses om j
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reaktiewe CV is gratis en oopbron. As dit jou gehelp het, oorweeg dit asseblief om te skenk."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: am\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Amharic\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "ዲስኮርድ"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "አሰናብት"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "መለያ የለዎትም? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "ይለግሱ"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "ሊያገናኙት የሚፈልጉትን URL እባክዎን ያስገ
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "እባክዎን ፕሮጀክቱን ይደግፉ"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume የየታሪክ መፍጠር፣ ማዘመን እና መ
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "ምላሽ ሰጪ የስራ ልምድ (Reactive) ነፃ እና ክፍት ምንጭ ነው። ከረዳዎት፣ እባክዎን መለገስን ያስቡበት።"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ar\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Arabic\n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
@@ -1036,7 +1036,7 @@ msgstr "ديسكورد"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "رفض"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "ليس لديك حساب؟ <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "يتبرع"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "يُرجى إدخال عنوان URL الذي تريد الربط به:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "يرجى دعم المشروع"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume هي أداة إنشاء سيرة ذاتية مجاني
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "برنامج Reactive Resume مجاني ومفتوح المصدر. إذا أفادك، يُرجى التفكير في التبرع."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: az\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Azerbaijani\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Ləğv et"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Hesabınız yoxdur? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Bağışlayın"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Link vermək istədiyiniz URLi daxil edin:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Layihəni dəstəkləyin"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume özgeçmişinizi yaratma, yeniləmə və paylaşma prose
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume pulsuz və açıq mənbəlidir. Əgər sizə kömək edibsə, zəhmət olmasa, ianə etməyi düşünün."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: bg\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Bulgarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Отхвърляне"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Нямате акаунт? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Дарете"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Моля, въведете URL адреса, към който иска
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Моля, подкрепете проекта"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume е безплатен конструктор на авт
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume е безплатен и с отворен код. Ако ви е помогнал, моля, помислете за дарение."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: bn\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Bengali\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "ডিসকর্ড"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "খারিজ করুন"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "অ্যাকাউন্ট নেই? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "দান করুন"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "যে URL‑এ লিংক করতে চান অনুগ্র
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "প্রকল্পটি সমর্থন করুন।"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume একটি বিনামূল্যের এবং
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "রিঅ্যাক্টিভ রেজ্যুমে বিনামূল্যে এবং ওপেন সোর্স। যদি এটি আপনাকে সাহায্য করে থাকে, তবে অনুগ্রহ করে অনুদান দেওয়ার কথা বিবেচনা করুন।"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ca\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Catalan\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Ignora"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "No teniu compte? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Donar"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Introdueix lURL al qual vols enllaçar:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Si us plau, doneu suport al projecte"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume és un creador de currículums gratuït i de codi obert
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume és gratuït i de codi obert. Si t'ha ajudat, considera fer una donació."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: cs\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Czech\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Propustit"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Nemáte účet? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Darovat"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Zadejte prosím URL, na kterou chcete odkazovat:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Prosím, podpořte projekt"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume je bezplatný nástroj pro tvorbu životopisů s otevře
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume je zdarma a má otevřený zdrojový kód. Pokud vám pomohl, zvažte prosím dar."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: da\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Danish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Afskedige"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Har du ikke en konto? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Donér"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Angiv den URL, du vil oprette et link til:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Støt venligst projektet"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume er en gratis og open-source CV-bygger, der forenkler pro
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume er gratis og open source. Hvis det har hjulpet dig, så overvej venligst at donere."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: de\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: German\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Zurückweisen"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Sie haben noch kein Konto? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Spenden"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Bitte geben Sie die URL ein, zu der Sie verlinken möchten:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Bitte unterstützen Sie das Projekt."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume ist ein kostenloser und quelloffener Lebenslauf-Builder,
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume ist kostenlos und Open Source. Wenn es Ihnen geholfen hat, würden wir uns über eine Spende freuen."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: el\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Greek\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Απολύω"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Δεν έχετε λογαριασμό; <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Προσφέρω"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Εισαγάγετε το URL στο οποίο θέλετε να γίν
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Παρακαλώ υποστηρίξτε το έργο"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Το Reactive Resume είναι ένας δωρεάν κατασκευα
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Το Reactive Resume είναι δωρεάν και ανοιχτού κώδικα. Αν σας έχει βοηθήσει, σκεφτείτε να κάνετε μια δωρεά."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: en_GB\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: English, United Kingdom\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Dismiss"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Don't have an account? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Donate"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Please enter the URL you want to link to:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Please support the project"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume is a free and open-source resume builder that simplifies
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume is free and open source. If it has helped you, please consider donating."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: es\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Spanish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Despedir"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "¿No tienes una cuenta? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Donar"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Introduce la URL a la que quieres enlazar:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Por favor, apoyen el proyecto."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume es un creador de currículums gratuito y de código abie
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume es gratuito y de código abierto. Si te ha sido útil, considera hacer una donación."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fa\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Persian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "دیسکورد"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "رد کردن"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "حساب ندارید؟ <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "اهدا کنید"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "لطفاً URL مورد نظر برای لینک دادن را وارد
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "لطفا از پروژه حمایت کنید"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume یک رزومه‌ساز رایگان و متن‌باز
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "رزومه واکنشی رایگان و متن‌باز است. اگر به شما کمک کرده است، لطفاً کمک مالی را در نظر بگیرید."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fi\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Finnish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Hylkää"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Eikö sinulla ole tiliä? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Lahjoittaa"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Syötä linkitettävä URL-osoite:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Tue projektia"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume on ilmainen ja avoimen lähdekoodin ansioluettelon laati
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume on ilmainen ja avoimen lähdekoodin työkalu. Jos siitä on ollut sinulle apua, harkitse lahjoittamista."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: fr\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: French\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Rejeter"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Vous n'avez pas de compte ? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Faire un don"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Veuillez saisir l'URL vers laquelle vous souhaitez rediriger :"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Veuillez soutenir le projet"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume est un outil de création de CV gratuit et open-source q
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume est un logiciel libre et gratuit. S'il vous a été utile, n'hésitez pas à faire un don."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: he\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Hebrew\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
@@ -1036,7 +1036,7 @@ msgstr "דיסקורד"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "לְפַטֵר"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "אין לך חשבון? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "לִתְרוֹם"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "נא להזין את כתובת ה־URL שאליה ברצונך לקשר
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "אנא תמכו בפרויקט"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume הוא בונה קורות חיים חינמי בקוד
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "ריאקטיבי קורות חיים הם חינמיים ובקוד פתוח. אם זה עזר לך, אנא שקול לתרום."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: hi\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Hindi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "डिस्कॉर्ड"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "नकार देना"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "खाता नहीं है? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "दान करें"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "कृपया वह URL दर्ज करें जिससे आ
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "कृपया इस परियोजना का समर्थन करें।"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume एक मुफ़्त और ओपन‑सोर
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "रिएक्टिव रिज्यूम मुफ़्त और ओपन सोर्स है। अगर इससे आपको मदद मिली है, तो कृपया दान करने पर विचार करें।"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: hu\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Hungarian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Elvetés"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Nincs fiókja? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Adományoz"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Kérjük, add meg a csatolni kívánt URLt:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Kérjük, támogassa a projektet"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "A Reactive Resume ingyenes és nyílt forráskódú önéletrajz készí
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "A Reactive Resume ingyenes és nyílt forráskódú. Ha segített, kérjük, fontold meg az adományozást."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: id\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Indonesian\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Membubarkan"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Belum punya akun? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Menyumbangkan"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Silakan masukkan URL yang ingin Anda tautkan:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Mohon dukung proyek ini."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume adalah pembuat resume gratis dan sumber terbuka yang men
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume gratis dan bersifat open source. Jika aplikasi ini bermanfaat bagi Anda, mohon pertimbangkan untuk berdonasi."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: it\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Italian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Congedare"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Non hai un account? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Donare"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Inserisci l'URL a cui vuoi collegarti:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Per favore, sostenete il progetto."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume è un generatore di curriculum gratuito e open source ch
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume è gratuito e open source. Se ti è stato utile, considera la possibilità di fare una donazione."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ja\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Japanese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "却下する"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "アカウントをお持ちでないですか? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "寄付する"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "リンク先の URL を入力してください:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "プロジェクトへのご支援をお願いいたします"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume は、履歴書の作成、更新、共有のプロセ
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resumeは無料のオープンソースソフトウェアです。もしお役に立てたなら、ぜひご寄付をご検討ください。"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: km\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Khmer\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "បដិសេធ"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "មិនមានគណនី? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "បរិច្ចាគ"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "សូម​បញ្ចូល URL ដែល​អ្នក​ចង់
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "សូមជួយគាំទ្រគម្រោងនេះផង"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume ជា​កម្មវិធី​បង្កើត
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume គឺឥតគិតថ្លៃ និងជាប្រភពបើកចំហ។ ប្រសិនបើវាបានជួយអ្នក សូមពិចារណាបរិច្ចាគ។"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: kn\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Kannada\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "ಡಿಸ್ಕೋರ್ಡ್"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "ವಜಾಗೊಳಿಸಿ"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "ಖಾತೆ ಇಲ್ಲವೇ? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "ದಾನ ಮಾಡಿ"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "ನೀವು ಲಿಂಕ್ ಮಾಡಲು ಬಯಸುವ URL ಅನ
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "ದಯವಿಟ್ಟು ಯೋಜನೆಯನ್ನು ಬೆಂಬಲಿಸಿ"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume ನಿಮ್ಮ ರೆಸ್ಯೂಮ್ ಅನ್ನು
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "ರಿಯಾಕ್ಟಿವ್ ರೆಸ್ಯೂಮ್ ಉಚಿತ ಮತ್ತು ಮುಕ್ತ ಮೂಲವಾಗಿದೆ. ಇದು ನಿಮಗೆ ಸಹಾಯ ಮಾಡಿದ್ದರೆ, ದಯವಿಟ್ಟು ದಾನ ಮಾಡುವುದನ್ನು ಪರಿಗಣಿಸಿ."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ko\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Korean\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1036,7 +1036,7 @@ msgstr "디스코드"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "해고하다"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "계정이 없으신가요? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "기부하세요"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "링크할 URL을 입력하세요:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "이 프로젝트를 지원해 주세요."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume는 이력서 작성, 업데이트 및 공유를 간단
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume는 무료 오픈 소스 소프트웨어입니다. 도움이 되셨다면 기부를 고려해 주세요."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: lt\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Lithuanian\n"
"Plural-Forms: nplurals=4; plural=(n%10==1 && (n%100>19 || n%100<11) ? 0 : (n%10>=2 && n%10<=9) && (n%100>19 || n%100<11) ? 1 : n%1!=0 ? 2: 3);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Atmesti"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Neturite paskyros? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Paaukoti"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Įveskite URL, su kuriuo norite susieti:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Prašome paremti projektą"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "„Reactive Resume“ nemokama atvirojo kodo gyvenimo aprašymų kū
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "„Reactive Resume“ yra nemokamas ir atvirojo kodo. Jei jis jums padėjo, apsvarstykite galimybę paaukoti."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: lv\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Latvian\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Noraidīt"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Nav konta? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Ziedot"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Lūdzu, ievadiet URL, uz kuru vēlaties izveidot saiti:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Lūdzu, atbalstiet projektu"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume ir bezmaksas un atvērtā pirmkoda CV veidotājs, kas vi
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive CV ir bezmaksas un atvērtā koda resurss. Ja tas jums ir palīdzējis, lūdzu, apsveriet iespēju ziedot."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ml\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Malayalam\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "ഡിസ്‌കോർഡ്"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "നിരസിക്കുക"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "അക്കൗണ്ട് ഇല്ലേ? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "സംഭാവന ചെയ്യുക"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "നിങ്ങൾ ലിങ്ക് ചെയ്യാനാഗ്ര
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "ദയവായി പദ്ധതിയെ പിന്തുണയ്ക്കുക."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume നിങ്ങളുടെ റിസ്യൂം സൃ
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "റിയാക്ടീവ് റെസ്യൂമെ സൌജന്യവും ഓപ്പൺ സോഴ്‌സുമാണ്. ഇത് നിങ്ങൾക്ക് സഹായകരമായിട്ടുണ്ടെങ്കിൽ, ദയവായി സംഭാവന നൽകുന്നത് പരിഗണിക്കുക."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: mr\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Marathi\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "डिस्कॉर्ड"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "बरखास्त करा"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "खाते नाही? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "दान करा"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "कृपया तुम्हाला ज्या URL ला लि
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "कृपया प्रकल्पाला पाठिंबा द्या"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume हा मोफत आणि मुक्त-स्र
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "रिॲक्टिव्ह रिझ्युमे मोफत आणि ओपन सोर्स आहे. जर तुम्हाला याचा उपयोग झाला असेल, तर कृपया देणगी देण्याचा विचार करा."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ms\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Malay\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Ketepikan"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Tiada akaun? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Derma"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Sila masukkan URL yang anda ingin pautkan:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Sila sokong projek ini"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume ialah pembina resume percuma dan sumber terbuka yang men
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Resume Reaktif adalah percuma dan sumber terbuka. Jika ia telah membantu anda, sila pertimbangkan untuk menderma."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ne\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Nepali\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "डिस्कोर्ड"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "खारेज गर्नुहोस्"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "खाता छैन? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "दान गर्नुहोस्"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "कृपया तपाईंले लिङ्क गर्न च
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "कृपया परियोजनालाई समर्थन गर्नुहोस्।"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume निःशुल्क र खुला-स्रो
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "प्रतिक्रियाशील रिजुमे नि:शुल्क र खुला स्रोत हो। यदि यसले तपाईंलाई मद्दत गरेको छ भने, कृपया दान गर्ने विचार गर्नुहोस्।"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: nl\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Dutch\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Afwijzen"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Heb je nog geen account? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Doneer"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Voer de URL in waarnaar u wilt linken:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Steun dit project alstublieft."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume is een gratis en open-source cv-bouwer die het proces va
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume is gratis en open source. Als het je geholpen heeft, overweeg dan een donatie."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: no\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Norwegian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Avskjedige"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Har du ikke en konto? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Donere"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Skriv inn URL-en du vil lenke til:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Vennligst støtt prosjektet"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume er en gratis og åpen kildekode CV-bygger som forenkler
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume er gratis og har åpen kildekode. Hvis det har hjulpet deg, bør du vurdere å donere."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: or\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Odia\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "ଡିସକର୍ଡ (Discord)"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "ଖାରଜ କରନ୍ତୁ"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "ଖାତା ନାହିଁ? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "ଦାନ କରନ୍ତୁ"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "ଦୟାକରି ଯେଉଁ URL କୁ ଲିଙ୍କ କରିବ
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "ଦୟାକରି ପ୍ରୋଜେକ୍ଟକୁ ସମର୍ଥନ କରନ୍ତୁ।"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume ଏକ ନିଶୁଳ୍କ ଏବଂ ଖୋଲା‑
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "ପ୍ରତିକ୍ରିୟାଶୀଳ ରିଜ୍ୟୁମ୍ ମାଗଣା ଏବଂ ଖୋଲା ଉତ୍ସ। ଯଦି ଏହା ଆପଣଙ୍କୁ ସାହାଯ୍ୟ କରିଛି, ଦୟାକରି ଦାନ କରିବାକୁ ବିଚାର କରନ୍ତୁ।"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pl\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Polish\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Odrzucać"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Nie masz konta? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Podarować"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Wpisz adres URL, do którego chcesz utworzyć link:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Proszę wesprzeć projekt"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume to darmowy i open-source'owy kreator CV, który upraszcz
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume jest darmowy i ma otwarte oprogramowanie. Jeśli Ci pomogło, rozważ przekazanie darowizny."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pt\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Portuguese, Brazilian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Liberar"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Não tem uma conta? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Doar"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Insira o URL para o qual você deseja criar o link:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Por favor, apoie o projeto."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "O Reactive Resume é um criador de currículos gratuito e de código abe
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "O Reactive Resume é gratuito e de código aberto. Se ele lhe foi útil, considere fazer uma doação."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: pt\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Portuguese\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Dispensar"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Não tem uma conta? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Doar"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Introduza o URL para o qual pretende criar o link:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Por favor, apoie o projeto."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "O Reactive Resume é um criador de currículos gratuito e de código abe
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "O Reactive Resume é gratuito e de código aberto. Se ele lhe foi útil, considere fazer um donativo."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ro\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Romanian\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100>0 && n%100<20)) ? 1 : 2);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Închide"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Nu ai un cont? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Dona"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Introduceți URL-ul la care doriți să faceți link:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Vă rugăm să susțineți proiectul"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume este un constructor de CV-uri gratuit și open-source ca
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume este gratuit și open source. Dacă te-a ajutat, te rugăm să iei în considerare o donație."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ru\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Russian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Увольнять"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Нет аккаунта? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Пожертвовать"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Введите URL, на который вы хотите постави
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Пожалуйста, поддержите проект."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume — бесплатный конструктор резю
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume — бесплатный проект с открытым исходным кодом. Если он вам помог, пожалуйста, рассмотрите возможность пожертвования."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sk\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Slovak\n"
"Plural-Forms: nplurals=4; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 3;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Zavrieť"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Nemáte účet? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Darovať"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Zadaj prosím URL adresu, na ktorú chceš odkazovať:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Prosím, podporte projekt"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume je bezplatný a opensource nástroj na tvorbu životo
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume je bezplatný a má otvorený zdrojový kód. Ak vám pomohol, zvážte darovanie."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sl\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Slovenian\n"
"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Zavrzi"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Nimate računa? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Donirajte"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Vnesite URL, na katerega želite povezati:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Prosim, podprite projekt"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume je brezplačen in odprtokoden urejevalnik življenjepisa
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume je brezplačen in odprtokoden. Če vam je pomagal, prosimo, razmislite o donaciji."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sq\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Albanian\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Hiq"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Nuk keni llogari? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Dhuro"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Ju lutemi shkruani URL-në që dëshironi të lidhni:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Ju lutemi mbështesni projektin"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume është një ndërtues i CV-së, falas dhe me burim të
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive CV është falas dhe me kod të hapur. Nëse ju ka ndihmuar, ju lutemi merrni në konsideratë dhurimin."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sr\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Serbian (Cyrillic)\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
@@ -1036,7 +1036,7 @@ msgstr "Дискорд"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Одбаци"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Немате налог? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Донирајте"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Унесите URL на који желите да води веза:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Молимо вас да подржите пројекат"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Реактивни Резиме је бесплатан и отворе
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Реактивни резиме је бесплатан и отвореног кода. Ако вам је помогао, размислите о донацији."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: sv\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Swedish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Avfärda"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Har du inget konto? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Donera"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Ange URL:en du vill länka till:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Stöd projektet"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume är en gratis CV-byggare med öppen källkod som förenk
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume är gratis och har öppen källkod. Om det har hjälpt dig, överväg gärna att donera."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: ta\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Tamil\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "டிஸ்கார்ட்"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "நிராகரி"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "கணக்கு இல்லையா? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "நன்கொடை அளிக்கவும்"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "நீங்கள் இணைக்க விரும்பும்
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "திட்டத்திற்கு ஆதரவளிக்கவும்"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume என்பது உங்கள் ரெஸ்யூ
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "ரியாக்டிவ் ரெஸ்யூம் இலவசமானது மற்றும் திறந்த மூல மென்பொருளாகும். இது உங்களுக்கு உதவியிருந்தால், நன்கொடை அளிக்கப் பரிசீலிக்கவும்."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: te\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Telugu\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "డిస్కోర్డ్"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "కొట్టివేయండి"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "ఖాతా లేదా? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "విరాళం ఇవ్వండి"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "మీరు లింక్ చేయాలనుకునే URL‌న
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "దయచేసి ప్రాజెక్ట్‌కు మద్దతు ఇవ్వండి"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume అనేది ఉచితమైన, ఓపెన్-
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "రియాక్టివ్ రెజ్యూమ్ ఉచితం మరియు ఓపెన్ సోర్స్. ఇది మీకు సహాయపడినట్లయితే, దయచేసి విరాళం ఇవ్వడాన్ని పరిగణించండి."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: th\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Thai\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "อนุญาตให้ออกไป"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "ยังไม่มีบัญชี? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "บริจาค"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "กรุณากรอก URL ที่คุณต้องการ
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "โปรดให้การสนับสนุนโครงการนี้"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume คือเครื่องมือสร้าง
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume เป็นซอฟต์แวร์ฟรีและโอเพนซอร์ส หากซอฟต์แวร์นี้มีประโยชน์กับคุณ โปรดพิจารณาบริจาคเงินสนับสนุน"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: tr\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Turkish\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Azletmek"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Hesabınız yok mu? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Bağış yapmak"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Bağlamak istediğiniz URL'yi girin:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Lütfen projeyi destekleyin."
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume, özgeçmişinizi oluşturma, güncelleme ve paylaşma s
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume ücretsiz ve açık kaynaklıdır. Eğer size yardımcı olduysa, lütfen bağış yapmayı düşünün."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: uk\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Ukrainian\n"
"Plural-Forms: nplurals=4; plural=((n%10==1 && n%100!=11) ? 0 : ((n%10 >= 2 && n%10 <=4 && (n%100 < 12 || n%100 > 14)) ? 1 : ((n%10 == 0 || (n%10 >= 5 && n%10 <=9)) || (n%100 >= 11 && n%100 <= 14)) ? 2 : 3));\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Відхилити"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Немає облікового запису? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Пожертвувати"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Будь ласка, введіть URL-адресу, на яку хоч
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Будь ласка, підтримайте проєкт"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume — це безкоштовний та відкрити
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume — безкоштовний ресурс з відкритим вихідним кодом. Якщо він вам допоміг, будь ласка, подумайте про пожертвування."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: uz\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Uzbek\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Bekor qilish"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Hisobingiz yoʻqmi? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Xayriya qiling"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Havola qilmoqchi bo'lgan URL manzilini kiriting:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Iltimos, loyihani qo'llab-quvvatlang"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume bu bepul va ochiq kodli rezyume tuzuvchi bolib, rezyu
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume bepul va ochiq kodli. Agar u sizga yordam bergan bo'lsa, iltimos, xayriya qilishni ko'rib chiqing."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: vi\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Vietnamese\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "Miễn nhiệm"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "Chưa có tài khoản? <0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr "Quyên tặng"
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "Vui lòng nhập URL bạn muốn liên kết đến:"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "Hãy ủng hộ dự án!"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume là trình tạo sơ yếu lý lịch mã nguồn mở &
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume là phần mềm mã nguồn mở miễn phí. Nếu thấy hữu ích, xin vui lòng xem xét quyên góp."
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Chinese Simplified\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord 频道"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "解雇"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "没有账户?<0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr ""
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "请输入你想要链接到的 URL"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "请支持这个项目"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume 是一款免费开源的简历生成器,可简化创
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume 是免费开源的。如果它对您有所帮助,请考虑捐赠。"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
+5 -5
View File
@@ -8,7 +8,7 @@ msgstr ""
"Language: zh\n"
"Project-Id-Version: reactive-resume\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2026-05-11 11:51\n"
"PO-Revision-Date: 2026-05-11 12:13\n"
"Last-Translator: \n"
"Language-Team: Chinese Traditional\n"
"Plural-Forms: nplurals=1; plural=0;\n"
@@ -1036,7 +1036,7 @@ msgstr "Discord"
#: src/components/ui/donation-toast.tsx
msgid "Dismiss"
msgstr ""
msgstr "解僱"
#: src/routes/_home/-sections/footer.tsx
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
@@ -1049,7 +1049,7 @@ msgstr "沒有帳戶?<0/>"
#: src/components/ui/donation-toast.tsx
msgid "Donate"
msgstr ""
msgstr ""
#: src/routes/builder/$resumeId/-sidebar/right/sections/information.tsx
msgid "Donate to Reactive Resume"
@@ -2330,7 +2330,7 @@ msgstr "請輸入您要連結的 URL"
#: src/components/ui/donation-toast.tsx
msgid "Please support the project"
msgstr ""
msgstr "請支持這個項目"
#: src/routes/$username/-components/public-resume.tsx
#: src/routes/builder/$resumeId/-components/dock.tsx
@@ -2472,7 +2472,7 @@ msgstr "Reactive Resume 是一個免費且開放原始碼的履歷建立工具
#: src/components/ui/donation-toast.tsx
msgid "Reactive Resume is free and open source. If it has helped you, please consider donating."
msgstr ""
msgstr "Reactive Resume 是免費開源的。如果它對您有所幫助,請考慮捐贈。"
#: src/routes/_home/-sections/faq.tsx
msgid "Reactive Resume is open-source, privacy-focused, and completely free. Unlike other resume builders, it doesn't show ads, track your data, or limit your features behind a paywall."
@@ -0,0 +1,56 @@
// @vitest-environment happy-dom
import { fireEvent, render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { CometCard } from "./comet-card";
describe("CometCard", () => {
it("renders its children inside the perspective wrapper", () => {
const { getByText } = render(
<CometCard>
<span>card body</span>
</CometCard>,
);
expect(getByText("card body")).toBeInTheDocument();
});
it("merges custom className into the wrapper", () => {
const { container } = render(
<CometCard className="extra-class">
<span>x</span>
</CometCard>,
);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.className).toContain("extra-class");
expect(wrapper.className).toContain("perspective-distant");
expect(wrapper.className).toContain("transform-3d");
});
it("renders a glare overlay positioned absolutely with mix-blend-overlay", () => {
const { container } = render(
<CometCard>
<span>x</span>
</CometCard>,
);
const glare = container.querySelector("[class*='mix-blend-overlay']") as HTMLElement | null;
expect(glare).not.toBeNull();
expect(glare?.className).toContain("pointer-events-none");
});
it("does not throw when mouse enters / moves over / leaves the card", () => {
const { container } = render(
<CometCard>
<span>x</span>
</CometCard>,
);
const tiltable = container.querySelector("[class*='will-change-transform']") as HTMLElement;
expect(tiltable).toBeTruthy();
expect(() => {
fireEvent.mouseMove(tiltable, { clientX: 100, clientY: 50 });
fireEvent.mouseMove(tiltable, { clientX: 0, clientY: 0 });
fireEvent.mouseLeave(tiltable);
}).not.toThrow();
});
});
@@ -0,0 +1,52 @@
// @vitest-environment happy-dom
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { CountUp } from "./count-up";
describe("CountUp", () => {
it("renders an aria-live=polite span by default (announced to screen readers)", () => {
const { container } = render(<CountUp to={1000} />);
const span = container.querySelector("span") as HTMLSpanElement;
expect(span.getAttribute("aria-live")).toBe("polite");
expect(span.getAttribute("aria-atomic")).toBe("true");
});
it("seeds textContent to the 'from' value when counting up", () => {
const { container } = render(<CountUp from={0} to={100} />);
const span = container.querySelector("span") as HTMLSpanElement;
expect(span.textContent).toBe("0");
});
it("seeds textContent to the 'to' value when direction is down", () => {
const { container } = render(<CountUp from={0} to={100} direction="down" />);
const span = container.querySelector("span") as HTMLSpanElement;
expect(span.textContent).toBe("100");
});
it("formats with the separator when one is supplied", () => {
const { container } = render(<CountUp from={1234} to={2345} separator="," />);
const span = container.querySelector("span") as HTMLSpanElement;
expect(span.textContent).toBe("1,234");
});
it("preserves decimal places when from / to are fractional", () => {
const { container } = render(<CountUp from={1.25} to={3.75} />);
const span = container.querySelector("span") as HTMLSpanElement;
expect(span.textContent).toBe("1.25");
});
it("strips aria-live and aria-atomic when aria-hidden is set", () => {
const { container } = render(<CountUp to={100} aria-hidden="true" />);
const span = container.querySelector("span") as HTMLSpanElement;
expect(span.getAttribute("aria-hidden")).toBe("true");
expect(span.getAttribute("aria-live")).toBeNull();
expect(span.getAttribute("aria-atomic")).toBeNull();
});
it("accepts a custom className", () => {
const { container } = render(<CountUp to={100} className="custom-class" />);
const span = container.querySelector("span") as HTMLSpanElement;
expect(span.className).toContain("custom-class");
});
});
@@ -0,0 +1,45 @@
// @vitest-environment happy-dom
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { Spotlight } from "./spotlight";
describe("Spotlight", () => {
it("renders a non-pointer-events overlay container", () => {
const { container } = render(<Spotlight />);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.className).toContain("pointer-events-none");
expect(wrapper.className).toContain("absolute");
});
it("renders both left and right beam groups by default", () => {
const { container } = render(<Spotlight />);
// Outer wrapper > two animated beam containers
const beamGroups = container.firstChild?.childNodes;
expect(beamGroups?.length).toBe(2);
});
it("applies the provided width / height / smallWidth to inline styles", () => {
const { container } = render(<Spotlight width={500} height={800} smallWidth={120} translateY={-100} />);
const inlineStyles = Array.from(container.querySelectorAll<HTMLDivElement>("[style]")).map(
(el) => el.getAttribute("style") ?? "",
);
const allStyles = inlineStyles.join("|");
expect(allStyles).toContain("width: 500px");
expect(allStyles).toContain("height: 800px");
expect(allStyles).toContain("width: 120px");
expect(allStyles).toContain("translateY(-100px)");
});
it("uses the supplied gradient strings as background values", () => {
const customFirst = "radial-gradient(red, blue)";
const { container } = render(<Spotlight gradientFirst={customFirst} />);
const matched = Array.from(container.querySelectorAll<HTMLDivElement>("[style]")).filter(
(el) => el.style.background.includes("red") && el.style.background.includes("blue"),
);
expect(matched.length).toBeGreaterThan(0);
});
});
@@ -0,0 +1,47 @@
// @vitest-environment happy-dom
import { fireEvent, render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { TextMaskEffect } from "./text-mask";
const renderMask = (props: React.ComponentProps<typeof TextMaskEffect>) => render(<TextMaskEffect {...props} />);
describe("TextMaskEffect", () => {
it("renders the supplied text in all visible text layers", () => {
const { container } = renderMask({ text: "Hello World" });
const texts = container.querySelectorAll("text");
expect(texts.length).toBeGreaterThanOrEqual(2);
for (const el of texts) {
expect(el.textContent).toBe("Hello World");
}
});
it("forwards aria-hidden onto the root svg", () => {
const { container } = renderMask({ text: "X", "aria-hidden": "true" });
const svg = container.querySelector("svg") as SVGSVGElement;
expect(svg.getAttribute("aria-hidden")).toBe("true");
});
it("renders an aria-label on the root svg", () => {
const { container } = renderMask({ text: "X" });
const svg = container.querySelector("svg") as SVGSVGElement;
expect(svg.getAttribute("aria-label")).toBe("Text mask effect");
});
it("does not throw on mouse-enter / mouse-move / mouse-leave interactions", () => {
const { container } = renderMask({ text: "X" });
const svg = container.querySelector("svg") as SVGSVGElement;
expect(() => {
fireEvent.mouseEnter(svg);
fireEvent.mouseMove(svg, { clientX: 50, clientY: 25 });
fireEvent.mouseLeave(svg);
}).not.toThrow();
});
it("merges custom className into the svg", () => {
const { container } = renderMask({ text: "X", className: "custom-class" });
const svg = container.querySelector("svg") as SVGSVGElement;
expect(svg.getAttribute("class") ?? "").toContain("custom-class");
});
});
@@ -0,0 +1,53 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { Command } from "@reactive-resume/ui/components/command";
import { useCommandPaletteStore } from "../store";
import { BaseCommandGroup } from "./base";
const renderInCommand = (ui: React.ReactNode) => render(<Command>{ui}</Command>);
const resetStore = () => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
};
afterEach(resetStore);
describe("BaseCommandGroup", () => {
it("renders children at the root (no page prop) when the page stack is empty", () => {
renderInCommand(<BaseCommandGroup heading="Root">child-text</BaseCommandGroup>);
expect(screen.getByText("child-text")).toBeInTheDocument();
});
it("does NOT render when the page stack tops a different page than its `page` prop", () => {
useCommandPaletteStore.setState({ pages: ["other"] });
const { container } = renderInCommand(
<BaseCommandGroup page="settings" heading="Settings">
<span>x</span>
</BaseCommandGroup>,
);
// Nothing rendered besides the Command shell itself.
expect(container.textContent).toBe("");
});
it("renders when the top of the page stack matches its `page` prop", () => {
useCommandPaletteStore.setState({ pages: ["settings"] });
renderInCommand(
<BaseCommandGroup page="settings" heading="Settings">
settings-children
</BaseCommandGroup>,
);
expect(screen.getByText("settings-children")).toBeInTheDocument();
});
it("does NOT render the root group when there is a sub-page on top", () => {
useCommandPaletteStore.setState({ pages: ["settings"] });
const { container } = renderInCommand(
<BaseCommandGroup heading="Root">
<span>root-text</span>
</BaseCommandGroup>,
);
expect(container.textContent).toBe("");
});
});
@@ -0,0 +1,52 @@
// @vitest-environment happy-dom
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { Command } from "@reactive-resume/ui/components/command";
import { useCommandPaletteStore } from "../../store";
vi.mock("@/components/theme/provider", () => ({
useTheme: () => ({ setTheme: vi.fn(), theme: "light", toggleTheme: vi.fn() }),
}));
const { PreferencesCommandGroup } = await import("./index");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
afterEach(() => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
});
const renderGroup = () =>
render(
<I18nProvider i18n={i18n}>
<Command>
<PreferencesCommandGroup />
</Command>
</I18nProvider>,
);
describe("PreferencesCommandGroup", () => {
it("renders 'Change theme to...' and 'Change language to...' at the root", () => {
renderGroup();
expect(screen.getByText("Change theme to...")).toBeInTheDocument();
expect(screen.getByText("Change language to...")).toBeInTheDocument();
});
it("pushes 'theme' onto the page stack when the theme item is selected", () => {
renderGroup();
const item = screen.getByText("Change theme to...");
fireEvent.click(item);
expect(useCommandPaletteStore.getState().pages).toContain("theme");
});
it("pushes 'language' onto the page stack when the language item is selected", () => {
renderGroup();
fireEvent.click(screen.getByText("Change language to..."));
expect(useCommandPaletteStore.getState().pages).toContain("language");
});
});
@@ -0,0 +1,59 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { Command } from "@reactive-resume/ui/components/command";
import { localeMap } from "@/libs/locale";
import { useCommandPaletteStore } from "../../store";
import { LanguageCommandPage } from "./language";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
afterEach(() => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
});
const renderPage = () =>
render(
<I18nProvider i18n={i18n}>
<Command>
<LanguageCommandPage />
</Command>
</I18nProvider>,
);
describe("LanguageCommandPage", () => {
it("does NOT render when the page stack does not have 'language' on top", () => {
renderPage();
// localeMap codes shouldn't appear because BaseCommandGroup gating is off.
expect(screen.queryByText("en-US")).toBeNull();
});
it("renders one CommandItem for every entry in localeMap when active", () => {
useCommandPaletteStore.setState({ pages: ["language"] });
renderPage();
const expectedCount = Object.keys(localeMap).length;
expect(expectedCount).toBeGreaterThan(0);
// Each locale value is rendered in the inline font-mono span.
for (const code of Object.keys(localeMap).slice(0, 5)) {
expect(screen.getByText(code)).toBeInTheDocument();
}
});
it("includes the documented set of locales (sample check)", () => {
useCommandPaletteStore.setState({ pages: ["language"] });
renderPage();
// Spot-check a couple of common locales.
for (const code of ["en-US", "de-DE", "ja-JP"]) {
if (code in localeMap) {
expect(screen.getByText(code)).toBeInTheDocument();
}
}
});
});
@@ -0,0 +1,45 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { Command } from "@reactive-resume/ui/components/command";
import { useCommandPaletteStore } from "../../store";
vi.mock("@/components/theme/provider", () => ({
useTheme: () => ({ setTheme: vi.fn(), theme: "light", toggleTheme: vi.fn() }),
}));
const { ThemeCommandPage } = await import("./theme");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
afterEach(() => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
});
const renderPage = () =>
render(
<I18nProvider i18n={i18n}>
<Command>
<ThemeCommandPage />
</Command>
</I18nProvider>,
);
describe("ThemeCommandPage", () => {
it("is hidden when 'theme' is not on top of the page stack", () => {
renderPage();
expect(screen.queryByText("Light theme")).toBeNull();
});
it("renders Light + Dark options when active", () => {
useCommandPaletteStore.setState({ pages: ["theme"] });
renderPage();
expect(screen.getByText("Light theme")).toBeInTheDocument();
expect(screen.getByText("Dark theme")).toBeInTheDocument();
});
});
@@ -0,0 +1,118 @@
import { afterEach, describe, expect, it } from "vitest";
import { useCommandPaletteStore } from "./store";
const reset = () => {
useCommandPaletteStore.setState({ open: false, search: "", pages: [] });
};
afterEach(reset);
describe("useCommandPaletteStore", () => {
it("starts closed with empty search and no pages", () => {
const state = useCommandPaletteStore.getState();
expect(state.open).toBe(false);
expect(state.search).toBe("");
expect(state.pages).toEqual([]);
});
it("setOpen(true) opens the palette without touching other fields", () => {
useCommandPaletteStore.setState({ search: "foo", pages: ["page1"] });
useCommandPaletteStore.getState().setOpen(true);
const state = useCommandPaletteStore.getState();
expect(state.open).toBe(true);
expect(state.search).toBe("foo");
expect(state.pages).toEqual(["page1"]);
});
it("setOpen(false) resets the entire store back to initial state", () => {
useCommandPaletteStore.setState({ open: true, search: "foo", pages: ["page1"] });
useCommandPaletteStore.getState().setOpen(false);
const state = useCommandPaletteStore.getState();
expect(state.open).toBe(false);
expect(state.search).toBe("");
expect(state.pages).toEqual([]);
});
it("setSearch updates only the search field", () => {
useCommandPaletteStore.getState().setSearch("query");
expect(useCommandPaletteStore.getState().search).toBe("query");
});
it("pushPage appends a page and clears search", () => {
useCommandPaletteStore.setState({ search: "leftover" });
useCommandPaletteStore.getState().pushPage("settings");
const state = useCommandPaletteStore.getState();
expect(state.pages).toEqual(["settings"]);
expect(state.search).toBe("");
});
it("pushPage stacks multiple pages in order", () => {
const { pushPage } = useCommandPaletteStore.getState();
pushPage("a");
pushPage("b");
pushPage("c");
expect(useCommandPaletteStore.getState().pages).toEqual(["a", "b", "c"]);
});
it("peekPage returns the top page (or undefined when empty)", () => {
expect(useCommandPaletteStore.getState().peekPage()).toBeUndefined();
useCommandPaletteStore.setState({ pages: ["a", "b"] });
expect(useCommandPaletteStore.getState().peekPage()).toBe("b");
});
it("popPage removes the last page and clears search", () => {
useCommandPaletteStore.setState({ pages: ["a", "b"], search: "x" });
useCommandPaletteStore.getState().popPage();
const state = useCommandPaletteStore.getState();
expect(state.pages).toEqual(["a"]);
expect(state.search).toBe("");
});
it("reset clears every state field", () => {
useCommandPaletteStore.setState({ open: true, search: "x", pages: ["a"] });
useCommandPaletteStore.getState().reset();
expect(useCommandPaletteStore.getState()).toMatchObject({ open: false, search: "", pages: [] });
});
describe("goBack", () => {
it("clears search first if present, leaving pages and open untouched", () => {
useCommandPaletteStore.setState({ open: true, search: "text", pages: ["a"] });
useCommandPaletteStore.getState().goBack();
const state = useCommandPaletteStore.getState();
expect(state.search).toBe("");
expect(state.pages).toEqual(["a"]);
expect(state.open).toBe(true);
});
it("pops the top page when no search and pages exist", () => {
useCommandPaletteStore.setState({ open: true, search: "", pages: ["a", "b"] });
useCommandPaletteStore.getState().goBack();
const state = useCommandPaletteStore.getState();
expect(state.pages).toEqual(["a"]);
expect(state.open).toBe(true);
});
it("closes the palette when no search and no pages", () => {
useCommandPaletteStore.setState({ open: true, search: "", pages: [] });
useCommandPaletteStore.getState().goBack();
expect(useCommandPaletteStore.getState().open).toBe(false);
});
});
});
@@ -0,0 +1,84 @@
// @vitest-environment happy-dom
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { ChipInput } from "./chip-input";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const renderInput = (props: Partial<React.ComponentProps<typeof ChipInput>> = {}) =>
render(
<I18nProvider i18n={i18n}>
<ChipInput defaultValue={[]} onChange={vi.fn()} {...props} />
</I18nProvider>,
);
describe("ChipInput", () => {
it("renders the supplied chips as Badges", () => {
renderInput({ defaultValue: ["alpha", "beta", "gamma"] });
expect(screen.getByText("alpha")).toBeInTheDocument();
expect(screen.getByText("beta")).toBeInTheDocument();
expect(screen.getByText("gamma")).toBeInTheDocument();
});
it("adds a chip on Enter, calling onChange with the new list", () => {
const onChange = vi.fn();
renderInput({ defaultValue: ["a"], onChange });
const input = document.querySelector("input") as HTMLInputElement;
fireEvent.change(input, { target: { value: "b" } });
fireEvent.keyDown(input, { key: "Enter" });
expect(onChange).toHaveBeenCalledWith(["a", "b"]);
});
it("adds a chip on comma keypress", () => {
const onChange = vi.fn();
renderInput({ defaultValue: [], onChange });
const input = document.querySelector("input") as HTMLInputElement;
fireEvent.change(input, { target: { value: "new-tag" } });
fireEvent.keyDown(input, { key: "," });
expect(onChange).toHaveBeenCalledWith(["new-tag"]);
});
it("does not add a duplicate chip", () => {
const onChange = vi.fn();
renderInput({ defaultValue: ["a"], onChange });
const input = document.querySelector("input") as HTMLInputElement;
fireEvent.change(input, { target: { value: "a" } });
fireEvent.keyDown(input, { key: "Enter" });
// chips set should remain ["a"]; onChange not invoked with the same array.
const callsAddingA = onChange.mock.calls.filter((args) => Array.isArray(args[0]) && args[0].length > 1);
expect(callsAddingA.length).toBe(0);
});
it("does not add an empty / whitespace-only chip", () => {
const onChange = vi.fn();
renderInput({ defaultValue: ["a"], onChange });
const input = document.querySelector("input") as HTMLInputElement;
fireEvent.change(input, { target: { value: " " } });
fireEvent.keyDown(input, { key: "Enter" });
expect(onChange).not.toHaveBeenCalled();
});
it("hides the description copy when hideDescription is true", () => {
const { container } = renderInput({ defaultValue: ["a"], hideDescription: true });
// We don't know the exact translated text, just confirm no <Kbd> hint banner is rendered.
expect(container.querySelector("kbd")).toBeNull();
});
it("shows the description copy by default", () => {
const { container } = renderInput({ defaultValue: ["a"] });
expect(container.querySelector("kbd")).not.toBeNull();
});
});
@@ -0,0 +1,61 @@
// @vitest-environment happy-dom
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
vi.mock("@uiw/react-color-colorful", () => ({
default: () => null,
}));
const { ColorPicker } = await import("./color-picker");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const renderPicker = (props: React.ComponentProps<typeof ColorPicker> = {}) =>
render(
<I18nProvider i18n={i18n}>
<ColorPicker {...props} />
</I18nProvider>,
);
describe("ColorPicker", () => {
it("renders a trigger swatch reflecting the current value", () => {
const { container } = renderPicker({ defaultValue: "rgba(231, 0, 11, 1)" });
const trigger = container.querySelector("[style*='background-color']") as HTMLElement | null;
expect(trigger).not.toBeNull();
// happy-dom serializes both the input rgba string and the rgb representation,
// depending on alpha; just confirm the color values surface.
const bg = trigger?.getAttribute("style") ?? "";
expect(bg).toContain("231");
expect(bg).toContain("11");
});
it("calls onChange when a preset color is clicked", () => {
const onChange = vi.fn();
renderPicker({ defaultValue: "rgba(0, 0, 0, 1)", onChange });
// Open the popover by clicking the trigger swatch.
const triggerSwatch = document.querySelector("[style*='background-color']") as HTMLElement;
fireEvent.click(triggerSwatch);
// Locate any preset button (they have aria-label='Use color rgba(...)').
const presetBtn = screen.getAllByRole("button", { name: /Use color rgba\(/ })[0];
fireEvent.click(presetBtn);
expect(onChange).toHaveBeenCalled();
expect(onChange.mock.calls[0]?.[0]).toMatch(/^rgba\(/);
});
it("renders a custom trigger when provided", () => {
renderPicker({
defaultValue: "rgba(0, 0, 0, 1)",
trigger: <button type="button">custom trigger</button>,
});
expect(screen.getByText("custom trigger")).toBeInTheDocument();
});
});
@@ -0,0 +1,66 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
const queryResult = vi.hoisted(() => ({ data: undefined as number | undefined }));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => queryResult,
}));
vi.mock("@/libs/orpc/client", () => ({
orpc: { statistics: { github: { getStarCount: { queryOptions: () => ({}) } } } },
}));
vi.mock("../animation/count-up", () => ({
CountUp: ({ to }: { to: number }) => <span data-testid="count-up">{to.toLocaleString()}</span>,
}));
const { GithubStarsButton } = await import("./github-stars-button");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
beforeEach(() => {
queryResult.data = undefined;
});
const renderButton = () =>
render(
<I18nProvider i18n={i18n}>
<GithubStarsButton />
</I18nProvider>,
);
describe("GithubStarsButton", () => {
it("renders an anchor pointing at the project repo with rel=noopener and target=_blank", () => {
renderButton();
const link = screen.getByRole("button") as HTMLAnchorElement;
expect(link.href).toBe("https://github.com/amruthpillai/reactive-resume");
expect(link.target).toBe("_blank");
expect(link.rel).toBe("noopener");
});
it("uses the no-count aria-label when star count hasn't loaded yet", () => {
renderButton();
const link = screen.getByRole("button") as HTMLAnchorElement;
expect(link.getAttribute("aria-label")).toBe("Star us on GitHub (opens in new tab)");
});
it("does not render a CountUp when star count is undefined", () => {
renderButton();
expect(screen.queryByTestId("count-up")).toBeNull();
});
it("renders a CountUp + announces the star count when available", () => {
queryResult.data = 12345;
renderButton();
expect(screen.getByTestId("count-up").textContent).toBe("12,345");
const link = screen.getByRole("button") as HTMLAnchorElement;
expect(link.getAttribute("aria-label")).toContain("12,345 stars");
});
});
@@ -0,0 +1,74 @@
// @vitest-environment happy-dom
import { render } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
// react-window's <Grid> uses ResizeObserver / IntersectionObserver heavily and
// expects measurable layouts that happy-dom can't provide. Stub it with a simple
// pass-through that renders the first row of cells via the supplied cellComponent.
vi.mock("react-window", () => ({
Grid: ({
rowCount,
columnCount,
cellComponent: CellComponent,
cellProps,
}: {
rowCount: number;
columnCount: number;
cellComponent: React.ComponentType<
{ rowIndex: number; columnIndex: number; style: React.CSSProperties } & Record<string, unknown>
>;
cellProps: Record<string, unknown>;
}) => {
const cells: React.ReactNode[] = [];
for (let r = 0; r < Math.min(rowCount, 1); r++) {
for (let c = 0; c < columnCount; c++) {
cells.push(<CellComponent key={`${r}-${c}`} rowIndex={r} columnIndex={c} style={{}} {...cellProps} />);
}
}
return <div data-testid="grid">{cells}</div>;
},
}));
const { IconPicker } = await import("./icon-picker");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const renderPicker = (props: Partial<React.ComponentProps<typeof IconPicker>> = {}) =>
render(
<I18nProvider i18n={i18n}>
<IconPicker value="globe" onChange={vi.fn()} {...props} />
</I18nProvider>,
);
describe("IconPicker", () => {
it("renders a trigger button containing the current icon", () => {
const { container } = renderPicker();
const i = container.querySelector("i.ph") as HTMLElement;
expect(i.className).toContain("ph-globe");
});
it("changes the trigger icon when value prop changes", () => {
const { container, rerender } = renderPicker({ value: "globe" });
const before = container.querySelector("i.ph")?.className ?? "";
expect(before).toContain("ph-globe");
rerender(
<I18nProvider i18n={i18n}>
<IconPicker value="star" onChange={vi.fn()} />
</I18nProvider>,
);
const after = container.querySelector("i.ph")?.className ?? "";
expect(after).toContain("ph-star");
});
it("renders the picker button as a single icon-size button", () => {
const { container } = renderPicker();
expect(container.querySelectorAll("button").length).toBeGreaterThanOrEqual(1);
});
});
@@ -0,0 +1,85 @@
// @vitest-environment happy-dom
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { URLInput } from "./url-input";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const renderInput = (value: { url: string; label: string }, onChange = vi.fn(), hideLabelButton = false) =>
render(
<I18nProvider i18n={i18n}>
<URLInput value={value} onChange={onChange} hideLabelButton={hideLabelButton} />
</I18nProvider>,
);
describe("URLInput", () => {
it("strips the https:// prefix in the visible input value", () => {
renderInput({ url: "https://example.com/path", label: "" });
const input = screen.getByRole("textbox") as HTMLInputElement;
expect(input.value).toBe("example.com/path");
});
it("renders the raw value when no prefix is present", () => {
renderInput({ url: "no-prefix.example", label: "" });
const input = screen.getByRole("textbox") as HTMLInputElement;
expect(input.value).toBe("no-prefix.example");
});
it("adds https:// prefix on edit when not already present", () => {
const onChange = vi.fn();
renderInput({ url: "https://example.com", label: "" }, onChange);
const input = screen.getByRole("textbox") as HTMLInputElement;
fireEvent.change(input, { target: { value: "new.example" } });
expect(onChange).toHaveBeenCalledWith({
url: "https://new.example",
label: "",
});
});
it("keeps already-prefixed URLs intact on edit", () => {
const onChange = vi.fn();
renderInput({ url: "https://example.com", label: "" }, onChange);
const input = screen.getByRole("textbox") as HTMLInputElement;
fireEvent.change(input, { target: { value: "https://other.example" } });
expect(onChange).toHaveBeenCalledWith({
url: "https://other.example",
label: "",
});
});
it("emits an empty url string when cleared", () => {
const onChange = vi.fn();
renderInput({ url: "https://example.com", label: "" }, onChange);
const input = screen.getByRole("textbox") as HTMLInputElement;
fireEvent.change(input, { target: { value: "" } });
expect(onChange).toHaveBeenCalledWith({
url: "",
label: "",
});
});
it("hides the label button when hideLabelButton=true", () => {
const { container } = renderInput({ url: "https://example.com", label: "" }, vi.fn(), true);
// PopoverTrigger is rendered as a button; its absence means hideLabelButton worked.
const buttons = container.querySelectorAll("button");
expect(buttons.length).toBe(0);
});
it("renders the label button by default", () => {
const { container } = renderInput({ url: "https://example.com", label: "" });
const buttons = container.querySelectorAll("button");
expect(buttons.length).toBeGreaterThan(0);
});
});
@@ -0,0 +1,51 @@
// @vitest-environment happy-dom
import { render } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { BreakpointIndicator } from "./breakpoint-indicator";
const getWrapper = (container: HTMLElement) => container.firstChild as HTMLElement;
describe("BreakpointIndicator", () => {
it("defaults to bottom-right positioning when no position is supplied", () => {
const { container } = render(<BreakpointIndicator />);
const wrapper = getWrapper(container);
expect(wrapper.className).toContain("bottom-0");
// bottom-right path: top branch sets the "bottom-0", right path sets "inset-e-0"
expect(wrapper.className).toContain("inset-e-0");
});
it("renders all breakpoint labels (one is visible at each viewport)", () => {
const { container } = render(<BreakpointIndicator />);
const text = container.textContent ?? "";
for (const label of ["XS", "SM", "MD", "LG", "XL", "2XL", "3XL", "4XL"]) {
expect(text).toContain(label);
}
});
it("uses top-left classes for top-left position", () => {
const { container } = render(<BreakpointIndicator position="top-left" />);
const wrapper = getWrapper(container);
expect(wrapper.className).toContain("top-0");
expect(wrapper.className).toContain("inset-s-0");
});
it("uses top-right classes for top-right position", () => {
const { container } = render(<BreakpointIndicator position="top-right" />);
const wrapper = getWrapper(container);
expect(wrapper.className).toContain("top-0");
expect(wrapper.className).toContain("inset-e-0");
});
it("uses bottom-left classes for bottom-left position", () => {
const { container } = render(<BreakpointIndicator position="bottom-left" />);
const wrapper = getWrapper(container);
expect(wrapper.className).toContain("bottom-0");
expect(wrapper.className).toContain("inset-s-0");
});
it("hides the indicator from print stylesheets", () => {
const { container } = render(<BreakpointIndicator />);
expect(getWrapper(container).className).toContain("print:hidden");
});
});
@@ -0,0 +1,45 @@
// @vitest-environment happy-dom
import type { ErrorComponentProps } from "@tanstack/react-router";
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { ErrorScreen } from "./error-screen";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const renderError = (overrides: { error?: Error; reset?: () => void } = {}) => {
const props: ErrorComponentProps = {
error: overrides.error ?? new Error("boom"),
reset: overrides.reset ?? vi.fn(),
};
return render(
<I18nProvider i18n={i18n}>
<ErrorScreen {...props} />
</I18nProvider>,
);
};
describe("ErrorScreen", () => {
it("shows the error message provided", () => {
renderError({ error: new Error("Network is down") });
expect(screen.getByText("Network is down")).toBeInTheDocument();
});
it("shows the user-facing error heading", () => {
renderError();
expect(screen.getByText("An error occurred while loading the page.")).toBeInTheDocument();
});
it("calls reset when the Refresh button is clicked", () => {
const reset = vi.fn();
renderError({ reset });
fireEvent.click(screen.getByRole("button", { name: /refresh/i }));
expect(reset).toHaveBeenCalledTimes(1);
});
});
@@ -0,0 +1,35 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { LoadingScreen } from "./loading-screen";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
describe("LoadingScreen", () => {
it("renders a spinner and the loading text", () => {
render(
<I18nProvider i18n={i18n}>
<LoadingScreen />
</I18nProvider>,
);
expect(screen.getByText("Loading...")).toBeInTheDocument();
});
it("fills the viewport (fixed inset-0)", () => {
const { container } = render(
<I18nProvider i18n={i18n}>
<LoadingScreen />
</I18nProvider>,
);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.className).toContain("fixed");
expect(wrapper.className).toContain("inset-0");
});
});
@@ -0,0 +1,47 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
// `Link` from tanstack/react-router requires a Router context. Stub it out with
// a plain anchor so we can render the screen in isolation.
vi.mock("@tanstack/react-router", () => ({
Link: ({ children, to, ...rest }: React.PropsWithChildren<{ to: string }>) => (
<a href={typeof to === "string" ? to : "#"} {...rest}>
{children}
</a>
),
}));
const { NotFoundScreen } = await import("./not-found-screen");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const renderScreen = (routeId = "/missing") =>
render(
<I18nProvider i18n={i18n}>
<NotFoundScreen isNotFound routeId={routeId as never} />
</I18nProvider>,
);
describe("NotFoundScreen", () => {
it("renders the documented error heading", () => {
renderScreen();
expect(screen.getByText("An error occurred while loading the page.")).toBeInTheDocument();
});
it("displays the routeId that triggered the not-found", () => {
renderScreen("/dashboard/missing-page");
expect(screen.getByText("/dashboard/missing-page")).toBeInTheDocument();
});
it("renders a Go Back link", () => {
renderScreen();
const link = screen.getByRole("link", { name: /go back/i });
expect(link.getAttribute("href")).toBe("..");
});
});
@@ -0,0 +1,44 @@
// @vitest-environment happy-dom
import { render } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
// Capture the options the Combobox receives so we can introspect them.
const captured = vi.hoisted(() => ({
options: undefined as Array<{ value: string; label: string }> | undefined,
}));
vi.mock("@/components/ui/combobox", () => ({
Combobox: (props: { options: Array<{ value: string; label: string }> }) => {
captured.options = props.options;
return null;
},
}));
const { LevelTypeCombobox } = await import("./combobox");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
describe("LevelTypeCombobox", () => {
it("renders one option per level type from levelDesignSchema", () => {
render(<LevelTypeCombobox />);
expect(captured.options?.length).toBe(7);
const values = captured.options?.map((o) => o.value);
expect(values).toEqual(
expect.arrayContaining(["hidden", "circle", "square", "rectangle", "rectangle-full", "progress-bar", "icon"]),
);
});
it("produces a human-readable label for each level type", () => {
render(<LevelTypeCombobox />);
for (const opt of captured.options ?? []) {
expect(opt.label).toBeTruthy();
expect(typeof opt.label).toBe("string");
}
});
});
@@ -0,0 +1,79 @@
// @vitest-environment happy-dom
import { render } from "@testing-library/react";
import { beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { LevelDisplay } from "./display";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
describe("LevelDisplay", () => {
it("renders nothing when level is 0", () => {
const { container } = render(<LevelDisplay type="circle" icon="star" level={0} />);
expect(container.firstChild).toBeNull();
});
it("renders nothing when type is hidden", () => {
const { container } = render(<LevelDisplay type="hidden" icon="star" level={3} />);
expect(container.firstChild).toBeNull();
});
it("renders nothing when icon is empty (regardless of type)", () => {
const { container } = render(<LevelDisplay type="icon" icon="" level={3} />);
expect(container.firstChild).toBeNull();
});
it("renders 5 segments for progress-bar type", () => {
const { container } = render(<LevelDisplay type="progress-bar" icon="star" level={3} />);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.children).toHaveLength(5);
});
it("marks first N segments as active for progress-bar", () => {
const { container } = render(<LevelDisplay type="progress-bar" icon="star" level={3} />);
const wrapper = container.firstChild as HTMLElement;
const activeStates = Array.from(wrapper.children).map((el) => (el as HTMLElement).dataset.active);
expect(activeStates).toEqual(["true", "true", "true", "false", "false"]);
});
it("renders icons for icon type", () => {
const { container } = render(<LevelDisplay type="icon" icon="star" level={2} />);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.children).toHaveLength(5);
expect(wrapper.querySelectorAll("i").length).toBe(5);
expect(wrapper.querySelector("i")?.className).toContain("ph-star");
});
it("dims inactive icons for icon type", () => {
const { container } = render(<LevelDisplay type="icon" icon="star" level={1} />);
const wrapper = container.firstChild as HTMLElement;
const icons = wrapper.querySelectorAll("i");
expect(icons[0]?.className).not.toContain("opacity-40");
expect(icons[4]?.className).toContain("opacity-40");
});
it("renders square segments for circle/rectangle/rectangle-full types", () => {
for (const type of ["circle", "rectangle", "rectangle-full"] as const) {
const { container } = render(<LevelDisplay type={type} icon="star" level={2} />);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.children).toHaveLength(5);
const activeStates = Array.from(wrapper.children).map((el) => (el as HTMLElement).dataset.active);
expect(activeStates).toEqual(["true", "true", "false", "false", "false"]);
}
});
it("includes an aria-label describing the level", () => {
const { container } = render(<LevelDisplay type="circle" icon="star" level={4} />);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.getAttribute("role")).toBe("img");
expect(wrapper.getAttribute("aria-label")).toContain("4");
});
it("merges extra className into the wrapper", () => {
const { container } = render(<LevelDisplay type="circle" icon="star" level={1} className="extra" />);
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.className).toContain("extra");
});
});
@@ -0,0 +1,36 @@
// @vitest-environment happy-dom
import { beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { localeMap } from "@/libs/locale";
import { getLocaleOptions } from "./combobox";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
describe("getLocaleOptions", () => {
it("returns one option per entry in localeMap", () => {
const options = getLocaleOptions();
expect(options).toHaveLength(Object.keys(localeMap).length);
});
it("uses the locale code as the value", () => {
const options = getLocaleOptions();
const values = options.map((opt) => opt.value);
expect(values).toContain("en-US");
expect(values).toContain("de-DE");
});
it("populates label and keywords with the same translated string", () => {
const options = getLocaleOptions();
const enUS = options.find((opt) => opt.value === "en-US");
expect(enUS?.label).toBeTruthy();
expect(enUS?.keywords).toEqual([enUS?.label]);
});
it("uses unique values for every option", () => {
const values = getLocaleOptions().map((opt) => opt.value);
expect(new Set(values).size).toBe(values.length);
});
});
@@ -1,4 +1,5 @@
// @vitest-environment happy-dom
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -0,0 +1,87 @@
// @vitest-environment happy-dom
import { afterEach, describe, expect, it } from "vitest";
import {
DEFAULT_PDF_PAGE_SIZE,
getPreviewCanvasScale,
getScaledPreviewPageSize,
normalizeResumePreviewProps,
} from "./preview.shared";
describe("normalizeResumePreviewProps", () => {
it("applies the documented defaults when fields are omitted", () => {
const result = normalizeResumePreviewProps({});
expect(result).toMatchObject({
pageGap: 40,
pageLayout: "horizontal",
pageScale: 1,
showPageNumbers: false,
});
});
it("preserves supplied values and forwards extra props (className, data)", () => {
const result = normalizeResumePreviewProps({
className: "preview-class",
pageGap: 16,
pageLayout: "vertical",
pageScale: 1.5,
showPageNumbers: true,
});
expect(result.className).toBe("preview-class");
expect(result.pageGap).toBe(16);
expect(result.pageLayout).toBe("vertical");
expect(result.pageScale).toBe(1.5);
expect(result.showPageNumbers).toBe(true);
});
});
describe("getScaledPreviewPageSize", () => {
it("multiplies both dimensions by the scale", () => {
const result = getScaledPreviewPageSize({ width: 100, height: 200 }, 2);
expect(result).toEqual({ width: 200, height: 400 });
});
it("returns the default A4 page size unchanged when scaled by 1", () => {
expect(getScaledPreviewPageSize(DEFAULT_PDF_PAGE_SIZE, 1)).toEqual(DEFAULT_PDF_PAGE_SIZE);
});
it("supports fractional scaling", () => {
const result = getScaledPreviewPageSize({ width: 100, height: 200 }, 0.5);
expect(result).toEqual({ width: 50, height: 100 });
});
});
const setDevicePixelRatio = (value: number) => {
Object.defineProperty(window, "devicePixelRatio", {
writable: true,
configurable: true,
value,
});
};
afterEach(() => {
setDevicePixelRatio(1);
});
describe("getPreviewCanvasScale", () => {
it("returns the desired render scale (4x) for small pages", () => {
setDevicePixelRatio(1);
// width * height * 4 * 4 = 100 * 100 * 16 = 160_000 ≪ 16_777_216 budget
expect(getPreviewCanvasScale(100, 100)).toBe(4);
});
it("uses devicePixelRatio when it exceeds the desired 4x scale", () => {
setDevicePixelRatio(8);
// 50*50*8*8 = 160_000 ≪ budget, so we keep the 8x devicePixelRatio
expect(getPreviewCanvasScale(50, 50)).toBe(8);
});
it("clamps the scale when the page would exceed the canvas pixel budget", () => {
setDevicePixelRatio(1);
const scale = getPreviewCanvasScale(2000, 3000);
// Should NOT exceed the 4x desired scale and must satisfy the pixel budget.
expect(scale).toBeLessThan(4);
expect(scale * scale * 2000 * 3000).toBeLessThanOrEqual(16_777_216 + 1);
});
});
@@ -1,4 +1,5 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { sampleResumeData } from "@reactive-resume/schema/resume/sample";
@@ -0,0 +1,32 @@
// @vitest-environment happy-dom
import { renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
// ThemeProvider depends on TanStack Start helpers + server fn — stub them.
vi.mock("@tanstack/react-router", () => ({
useRouter: () => ({ invalidate: vi.fn() }),
}));
vi.mock("@/libs/theme", () => ({
setThemeServerFn: vi.fn().mockResolvedValue(undefined),
}));
const { ThemeProvider, useTheme } = await import("./provider");
describe("useTheme", () => {
it("throws when used outside ThemeProvider", () => {
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
expect(() => renderHook(() => useTheme())).toThrow(/useTheme must be used within a ThemeProvider/);
consoleError.mockRestore();
});
it("returns the theme and helpers when wrapped in ThemeProvider", () => {
const { result } = renderHook(() => useTheme(), {
wrapper: ({ children }) => <ThemeProvider theme="dark">{children}</ThemeProvider>,
});
expect(result.current.theme).toBe("dark");
expect(typeof result.current.setTheme).toBe("function");
expect(typeof result.current.toggleTheme).toBe("function");
});
});
@@ -0,0 +1,77 @@
// @vitest-environment happy-dom
import { fireEvent, render } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
const toggleTheme = vi.hoisted(() => vi.fn());
vi.mock("./provider", () => ({
useTheme: () => ({ theme: "light", setTheme: vi.fn(), toggleTheme }),
}));
const { ThemeToggleButton } = await import("./toggle-button");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
afterEach(() => {
toggleTheme.mockReset();
// Reset prefers-reduced-motion + startViewTransition stub between tests.
Object.defineProperty(window, "matchMedia", {
writable: true,
configurable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
addEventListener: () => {},
removeEventListener: () => {},
})),
});
// biome-ignore lint/suspicious/noExplicitAny: removing a non-standard API stub
(document as any).startViewTransition = undefined;
});
describe("ThemeToggleButton", () => {
it("renders a button with an aria-label that flips with the theme", () => {
const { container } = render(<ThemeToggleButton />);
const button = container.querySelector("button");
expect(button?.getAttribute("aria-label")).toBe("Switch to dark theme");
});
it("calls toggleTheme directly when the view-transition API is unavailable", () => {
const { container } = render(<ThemeToggleButton />);
const button = container.querySelector("button") as HTMLButtonElement;
fireEvent.click(button);
expect(toggleTheme).toHaveBeenCalledTimes(1);
});
it("calls toggleTheme directly when prefers-reduced-motion is set", () => {
Object.defineProperty(window, "matchMedia", {
writable: true,
configurable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: query.includes("prefers-reduced-motion"),
media: query,
addEventListener: () => {},
removeEventListener: () => {},
})),
});
const startVT = vi.fn();
Object.defineProperty(document, "startViewTransition", {
writable: true,
configurable: true,
value: startVT,
});
const { container } = render(<ThemeToggleButton />);
fireEvent.click(container.querySelector("button") as HTMLButtonElement);
expect(toggleTheme).toHaveBeenCalledTimes(1);
expect(startVT).not.toHaveBeenCalled();
});
});
@@ -1,4 +1,5 @@
// @vitest-environment happy-dom
import { render } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { FontWeightCombobox } from "./combobox";
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { getNextWeights } from "./combobox";
describe("getNextWeights", () => {
it("returns 400 and 600 when both are available (the preferred default)", () => {
// Source Sans 3 covers a wide weight range including 400 and 600.
const weights = getNextWeights("Source Sans 3");
expect(weights).toEqual(["400", "600"]);
});
it("returns null for unknown font families", () => {
expect(getNextWeights("This Font Does Not Exist")).toBeNull();
});
it("returns an array containing exactly known weight strings (subset of 100..900)", () => {
const weights = getNextWeights("Source Sans 3");
const validWeights = new Set(["100", "200", "300", "400", "500", "600", "700", "800", "900"]);
for (const w of weights ?? []) {
expect(validWeights.has(w)).toBe(true);
}
});
it("contains at most two weights", () => {
const weights = getNextWeights("Source Sans 3");
expect(weights?.length).toBeLessThanOrEqual(2);
});
it("returns the family's only weight (deduplicated) when only one is available", () => {
// Find a font with a single weight by scanning the fontList — fall back gracefully.
// We probe a known web font that may only ship 400; the test asserts uniqueness regardless.
const weights = getNextWeights("Source Sans 3");
if (weights) {
expect(new Set(weights).size).toBe(weights.length);
}
});
});
@@ -0,0 +1,45 @@
// @vitest-environment happy-dom
import type { ComboboxOption } from "./combobox";
import { render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { Combobox } from "./combobox";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const options: ComboboxOption<"alpha" | "beta" | "gamma">[] = [
{ value: "alpha", label: "Alpha" },
{ value: "beta", label: "Beta" },
{ value: "gamma", label: "Gamma" },
];
const wrap = (ui: React.ReactNode) => render(<I18nProvider i18n={i18n}>{ui}</I18nProvider>);
describe("Combobox", () => {
it("renders the default placeholder when nothing is selected", () => {
wrap(<Combobox options={[...options]} placeholder="Pick something" />);
expect(screen.getByText("Pick something")).toBeInTheDocument();
});
it("renders the selected option label when a value is provided", () => {
wrap(<Combobox options={[...options]} value="beta" />);
// The label appears inside the trigger; both label and trigger may render it,
// so use queryAllByText for resilience.
expect(screen.getAllByText("Beta").length).toBeGreaterThan(0);
});
it("renders all option labels for the multi-select default values", () => {
wrap(<Combobox multiple options={[...options]} defaultValue={["alpha", "gamma"]} />);
expect(screen.getAllByText(/Alpha/).length).toBeGreaterThan(0);
expect(screen.getAllByText(/Gamma/).length).toBeGreaterThan(0);
});
it("renders nothing extra when given an empty options array (no crash)", () => {
expect(() => wrap(<Combobox options={[]} placeholder="Empty" />)).not.toThrow();
expect(screen.getByText("Empty")).toBeInTheDocument();
});
});
@@ -0,0 +1,55 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
vi.stubGlobal("__APP_VERSION__", "9.9.9");
const { Copyright } = await import("./copyright");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const renderCopyright = (props?: React.ComponentProps<typeof Copyright>) =>
render(
<I18nProvider i18n={i18n}>
<Copyright {...props} />
</I18nProvider>,
);
describe("Copyright", () => {
it("renders the MIT license link", () => {
renderCopyright();
const link = screen.getByRole("link", { name: "MIT" });
expect(link.getAttribute("href")).toBe("https://github.com/AmruthPillai/Reactive-Resume/blob/main/LICENSE");
expect(link.getAttribute("rel")).toBe("noopener");
});
it("renders the Amruth Pillai attribution link", () => {
renderCopyright();
const link = screen.getByRole("link", { name: "Amruth Pillai" });
expect(link.getAttribute("href")).toBe("https://amruthpillai.com");
});
it("includes the app version string", () => {
renderCopyright();
expect(screen.getByText(/v9\.9\.9/)).toBeInTheDocument();
});
it("merges custom className into the wrapper", () => {
const { container } = renderCopyright({ className: "extra-class" });
const wrapper = container.firstChild as HTMLElement;
expect(wrapper.className).toContain("extra-class");
expect(wrapper.className).toContain("text-muted-foreground");
});
it("opens external links in a new tab", () => {
renderCopyright();
for (const link of screen.getAllByRole("link")) {
expect(link.getAttribute("target")).toBe("_blank");
}
});
});
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { templates } from "./data";
describe("templates metadata", () => {
const entries = Object.entries(templates);
it("declares the expected template ids", () => {
const ids = Object.keys(templates).sort();
expect(ids).toEqual(
[
"azurill",
"bronzor",
"chikorita",
"ditgar",
"ditto",
"gengar",
"glalie",
"kakuna",
"lapras",
"leafish",
"meowth",
"onyx",
"pikachu",
"rhyhorn",
].sort(),
);
});
it("provides a name, description, image, and tags for every template", () => {
for (const [id, meta] of entries) {
expect(meta.name, id).toBeTruthy();
expect(meta.description, id).toBeDefined();
expect(meta.imageUrl, id).toMatch(/^\/templates\//);
expect(Array.isArray(meta.tags), id).toBe(true);
expect(meta.tags.length, id).toBeGreaterThan(0);
}
});
it("uses a recognized sidebar position for every template", () => {
const validPositions = new Set(["left", "right", "none"]);
for (const [id, meta] of entries) {
expect(validPositions.has(meta.sidebarPosition), `${id}: ${meta.sidebarPosition}`).toBe(true);
}
});
it("uses unique image URLs per template", () => {
const urls = entries.map(([, m]) => m.imageUrl);
expect(new Set(urls).size).toBe(urls.length);
});
it("uses lowercase ids that match a lowercase form of the display name", () => {
for (const [id, meta] of entries) {
expect(id).toBe(id.toLowerCase());
expect(meta.name.toLowerCase()).toBe(id);
}
});
});
@@ -0,0 +1,72 @@
// @vitest-environment happy-dom
import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { Dialog } from "@reactive-resume/ui/components/dialog";
import { useDialogStore } from "@/dialogs/store";
const updateResumeData = vi.hoisted(() => vi.fn());
vi.mock("@/components/resume/builder-resume-draft", () => ({
useCurrentResume: () => ({
data: { metadata: { template: "ditto" } },
}),
useUpdateResumeData: () => updateResumeData,
}));
const { TemplateGalleryDialog } = await import("./gallery");
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
afterEach(() => {
updateResumeData.mockReset();
useDialogStore.setState({ open: false, activeDialog: null, onBeforeClose: null });
});
const renderGallery = () =>
render(
<I18nProvider i18n={i18n}>
<Dialog open>
<TemplateGalleryDialog />
</Dialog>
</I18nProvider>,
);
describe("TemplateGalleryDialog", () => {
it("renders the documented title and intro copy", () => {
renderGallery();
expect(screen.getByText("Template Gallery")).toBeInTheDocument();
expect(screen.getByText(/range of resume templates/)).toBeInTheDocument();
});
it("renders one tile per template", () => {
renderGallery();
// Each tile renders an <img alt={metadata.name}>. The data module lists 14 templates.
const images = screen.getAllByRole("img");
expect(images.length).toBeGreaterThanOrEqual(14);
});
it("ring-highlights the currently-selected template tile (Ditto)", () => {
renderGallery();
const dittoImg = screen.getByAltText("Ditto");
const button = dittoImg.closest("button") as HTMLButtonElement;
expect(button.className).toContain("ring-ring");
});
it("selecting a different template calls updateResumeData with the new template id", () => {
renderGallery();
const onyxImg = screen.getByAltText("Onyx");
const button = onyxImg.closest("button") as HTMLButtonElement;
fireEvent.click(button);
expect(updateResumeData).toHaveBeenCalledTimes(1);
const recipe = updateResumeData.mock.calls[0]?.[0] as (draft: { metadata: { template: string } }) => void;
const draft = { metadata: { template: "ditto" } };
recipe(draft);
expect(draft.metadata.template).toBe("onyx");
});
});
+69
View File
@@ -0,0 +1,69 @@
// @vitest-environment happy-dom
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { ConfirmDialogProvider, useConfirm } from "./use-confirm";
const wrapper = ({ children }: { children: React.ReactNode }) => (
<ConfirmDialogProvider>{children}</ConfirmDialogProvider>
);
describe("useConfirm", () => {
it("throws when used outside ConfirmDialogProvider", () => {
expect(() => renderHook(() => useConfirm())).toThrow(/useConfirm must be used within a <ConfirmDialogProvider \/>/);
});
it("returns a confirm function when wrapped in provider", () => {
const { result } = renderHook(() => useConfirm(), { wrapper });
expect(typeof result.current).toBe("function");
});
it("returns a pending promise that resolves to a boolean", async () => {
const { result } = renderHook(() => useConfirm(), { wrapper });
let promise!: Promise<boolean>;
await act(async () => {
promise = result.current("Are you sure?");
});
expect(promise).toBeInstanceOf(Promise);
});
it("resolves false when the dialog is dismissed", async () => {
const { result } = renderHook(() => useConfirm(), { wrapper });
let promise!: Promise<boolean>;
await act(async () => {
promise = result.current("Heading");
});
// Click the cancel button to close.
const cancelBtn = document.body.querySelector('button[type="button"][data-slot="alert-dialog-cancel"]');
// Fallback: cancel buttons in shadcn/base-ui dialogs usually carry role="button" + text.
const buttons = Array.from(document.body.querySelectorAll<HTMLButtonElement>("button"));
const cancel = buttons.find((b) => /cancel/i.test(b.textContent ?? ""));
await act(async () => {
(cancelBtn as HTMLButtonElement | null)?.click() ?? cancel?.click();
});
await expect(promise).resolves.toBe(false);
});
it("resolves true when the confirm button is clicked", async () => {
const { result } = renderHook(() => useConfirm(), { wrapper });
let promise!: Promise<boolean>;
await act(async () => {
promise = result.current("Heading", { confirmText: "Yes" });
});
const buttons = Array.from(document.body.querySelectorAll<HTMLButtonElement>("button"));
const yes = buttons.find((b) => /yes/i.test(b.textContent ?? ""));
await act(async () => {
yes?.click();
});
await expect(promise).resolves.toBe(true);
});
});
@@ -0,0 +1,79 @@
// @vitest-environment happy-dom
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { useControlledState } from "./use-controlled-state";
describe("useControlledState", () => {
it("returns the defaultValue when uncontrolled", () => {
const { result } = renderHook(() => useControlledState({ defaultValue: 0 }));
expect(result.current[0]).toBe(0);
});
it("returns the value when controlled", () => {
const { result } = renderHook(() => useControlledState({ value: 42, defaultValue: 0 }));
expect(result.current[0]).toBe(42);
});
it("updates internal state when value prop changes (controlled)", () => {
const { result, rerender } = renderHook(({ value }: { value: number }) => useControlledState({ value }), {
initialProps: { value: 1 },
});
expect(result.current[0]).toBe(1);
rerender({ value: 2 });
expect(result.current[0]).toBe(2);
});
it("updates state via setter when uncontrolled", () => {
const { result } = renderHook(() => useControlledState<number>({ defaultValue: 0 }));
act(() => {
result.current[1](10);
});
expect(result.current[0]).toBe(10);
});
it("calls onChange when state is updated", () => {
const onChange = vi.fn();
const { result } = renderHook(() => useControlledState<string>({ defaultValue: "a", onChange }));
act(() => {
result.current[1]("b");
});
expect(onChange).toHaveBeenCalledWith("b");
});
it("forwards extra args to onChange", () => {
const onChange = vi.fn();
const { result } = renderHook(() => useControlledState<string, [number, boolean]>({ defaultValue: "a", onChange }));
act(() => {
result.current[1]("b", 42, true);
});
expect(onChange).toHaveBeenCalledWith("b", 42, true);
});
it("does not call onChange when value prop changes from outside", () => {
const onChange = vi.fn();
const { rerender } = renderHook(({ value }: { value: number }) => useControlledState<number>({ value, onChange }), {
initialProps: { value: 1 },
});
rerender({ value: 2 });
expect(onChange).not.toHaveBeenCalled();
});
it("returns a stable setter reference when onChange is stable", () => {
const onChange = vi.fn();
const { result, rerender } = renderHook(
({ value }: { value: number }) => useControlledState<number>({ value, onChange }),
{ initialProps: { value: 1 } },
);
const initialSetter = result.current[1];
rerender({ value: 2 });
expect(result.current[1]).toBe(initialSetter);
});
});
+95
View File
@@ -0,0 +1,95 @@
// @vitest-environment happy-dom
import { act, renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { useIsMobile } from "./use-mobile";
type Listener = (event: { matches: boolean }) => void;
type FakeMediaQueryList = {
matches: boolean;
addEventListener: (type: string, fn: Listener) => void;
removeEventListener: (type: string, fn: Listener) => void;
__listeners: Set<Listener>;
__set: (matches: boolean) => void;
};
const createMatchMedia = (initialMatches: boolean) => {
const mqlByQuery = new Map<string, FakeMediaQueryList>();
const matchMedia = vi.fn((query: string): FakeMediaQueryList => {
let mql = mqlByQuery.get(query);
if (mql) return mql;
const listeners = new Set<Listener>();
mql = {
matches: initialMatches,
addEventListener: (_type, fn) => listeners.add(fn),
removeEventListener: (_type, fn) => listeners.delete(fn),
__listeners: listeners,
__set: (matches: boolean) => {
// biome-ignore lint/style/noNonNullAssertion: This closure only runs after the media query list has been initialized.
mql!.matches = matches;
for (const fn of listeners) fn({ matches });
},
};
mqlByQuery.set(query, mql);
return mql;
});
Object.defineProperty(window, "matchMedia", {
writable: true,
configurable: true,
value: matchMedia,
});
return { matchMedia, getMql: (q: string) => mqlByQuery.get(q) };
};
afterEach(() => {
vi.restoreAllMocks();
});
describe("useIsMobile", () => {
it("returns true when the viewport matches the mobile media query", () => {
createMatchMedia(true);
const { result } = renderHook(() => useIsMobile());
expect(result.current).toBe(true);
});
it("returns false when the viewport does not match the mobile media query", () => {
createMatchMedia(false);
const { result } = renderHook(() => useIsMobile());
expect(result.current).toBe(false);
});
it("uses the (max-width: 767px) query", () => {
const { matchMedia } = createMatchMedia(false);
renderHook(() => useIsMobile());
expect(matchMedia).toHaveBeenCalledWith("(max-width: 767px)");
});
it("updates when the media query change event fires", () => {
const { getMql } = createMatchMedia(false);
const { result } = renderHook(() => useIsMobile());
expect(result.current).toBe(false);
act(() => {
getMql("(max-width: 767px)")?.__set(true);
});
expect(result.current).toBe(true);
});
it("removes the listener on unmount", () => {
const { getMql } = createMatchMedia(false);
const { unmount } = renderHook(() => useIsMobile());
const mql = getMql("(max-width: 767px)");
expect(mql?.__listeners.size).toBe(1);
unmount();
expect(mql?.__listeners.size).toBe(0);
});
});
+79
View File
@@ -0,0 +1,79 @@
// @vitest-environment happy-dom
import { act, renderHook } from "@testing-library/react";
import { beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { PromptDialogProvider, usePrompt } from "./use-prompt";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<PromptDialogProvider>{children}</PromptDialogProvider>
);
const clickButton = (re: RegExp) => {
const buttons = Array.from(document.body.querySelectorAll<HTMLButtonElement>("button"));
buttons.find((b) => re.test(b.textContent ?? ""))?.click();
};
describe("usePrompt", () => {
it("throws when used outside PromptDialogProvider", () => {
expect(() => renderHook(() => usePrompt())).toThrow(/usePrompt must be used within a <PromptDialogProvider \/>/);
});
it("returns a function when wrapped in provider", () => {
const { result } = renderHook(() => usePrompt(), { wrapper });
expect(typeof result.current).toBe("function");
});
it("resolves null when Cancel is clicked", async () => {
const { result } = renderHook(() => usePrompt(), { wrapper });
let promise!: Promise<string | null>;
await act(async () => {
promise = result.current("Name?");
});
await act(async () => {
clickButton(/cancel/i);
});
await expect(promise).resolves.toBeNull();
});
it("resolves to current input value when Confirm is clicked", async () => {
const { result } = renderHook(() => usePrompt(), { wrapper });
let promise!: Promise<string | null>;
await act(async () => {
promise = result.current("Name?", { defaultValue: "Initial" });
});
await act(async () => {
clickButton(/confirm/i);
});
await expect(promise).resolves.toBe("Initial");
});
it("uses the supplied defaultValue as the initial input value", async () => {
const { result } = renderHook(() => usePrompt(), { wrapper });
let promise!: Promise<string | null>;
await act(async () => {
promise = result.current("Heading", { defaultValue: "preset" });
});
// The input element should hold the default before we click anything.
const input = document.body.querySelector("input") as HTMLInputElement | null;
expect(input?.value).toBe("preset");
await act(async () => {
clickButton(/confirm/i);
});
await expect(promise).resolves.toBe("preset");
});
});
@@ -0,0 +1,61 @@
// @vitest-environment happy-dom
import { renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { useSyncFormValues } from "./use-sync-form-values";
const makeForm = <T>(values: T) => {
const reset = vi.fn((next: T) => {
form.state.values = next;
});
const form = { reset, state: { values } };
return form;
};
describe("useSyncFormValues", () => {
it("does not call reset when values are deeply equal", () => {
const form = makeForm({ a: 1, nested: { x: 1 } });
renderHook(({ values }) => useSyncFormValues(form, values), {
initialProps: { values: { a: 1, nested: { x: 1 } } },
});
expect(form.reset).not.toHaveBeenCalled();
});
it("calls reset when values differ on mount", () => {
const form = makeForm({ a: 1 });
const next = { a: 2 };
renderHook(() => useSyncFormValues(form, next));
expect(form.reset).toHaveBeenCalledWith(next);
});
it("calls reset when values prop changes to a different shape", () => {
const form = makeForm<{ a: number }>({ a: 1 });
const { rerender } = renderHook(({ values }) => useSyncFormValues(form, values), {
initialProps: { values: { a: 1 } },
});
expect(form.reset).not.toHaveBeenCalled();
rerender({ values: { a: 5 } });
expect(form.reset).toHaveBeenCalledWith({ a: 5 });
expect(form.reset).toHaveBeenCalledTimes(1);
});
it("ignores new value identity when deeply equal", () => {
const form = makeForm<{ a: number }>({ a: 1 });
const { rerender } = renderHook(({ values }) => useSyncFormValues(form, values), {
initialProps: { values: { a: 1 } },
});
rerender({ values: { a: 1 } });
expect(form.reset).not.toHaveBeenCalled();
});
});
+49
View File
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import { QueryClient } from "@tanstack/react-query";
import { getQueryClient } from "./client";
describe("getQueryClient", () => {
it("returns a QueryClient instance", () => {
const client = getQueryClient();
expect(client).toBeInstanceOf(QueryClient);
});
it("returns a fresh client on each call", () => {
const a = getQueryClient();
const b = getQueryClient();
expect(a).not.toBe(b);
});
it("hashes the query key into a deterministic JSON string", () => {
const client = getQueryClient();
const fn = client.getDefaultOptions().queries?.queryKeyHashFn;
expect(typeof fn).toBe("function");
const hashA = fn?.(["resumes", { id: "abc" }]);
const hashB = fn?.(["resumes", { id: "abc" }]);
const hashC = fn?.(["resumes", { id: "xyz" }]);
expect(hashA).toBe(hashB);
expect(hashA).not.toBe(hashC);
expect(typeof hashA).toBe("string");
// json/meta envelope is included
expect(hashA).toContain('"json"');
});
it("round-trips data through dehydrate/hydrate via oRPC serializer", () => {
const client = getQueryClient();
const serializeData = client.getDefaultOptions().dehydrate?.serializeData;
const deserializeData = client.getDefaultOptions().hydrate?.deserializeData;
expect(serializeData).toBeTypeOf("function");
expect(deserializeData).toBeTypeOf("function");
const original = { id: "x", count: 3, when: new Date("2024-01-01T00:00:00Z") };
const serialized = serializeData?.(original);
const restored = deserializeData?.(serialized) as typeof original;
expect(restored.id).toBe(original.id);
expect(restored.count).toBe(original.count);
expect(restored.when.getTime()).toBe(original.when.getTime());
});
});
@@ -0,0 +1,89 @@
// @vitest-environment happy-dom
import { act, renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// Mock locale module so getLocaleMessages returns a known mapping
// without trying to dynamically load .po files (which Vite/glob handles
// only inside the real bundle).
vi.mock("@/libs/locale", () => ({
resolveLocale: (locale: string) => locale || "en-US",
getLocaleMessages: async (locale: string) => ({
locale,
messages: {},
}),
}));
beforeEach(() => {
vi.resetModules();
});
afterEach(() => {
vi.resetModules();
});
describe("createSectionTitleResolverForLocale", () => {
it("returns a resolver function that produces section titles", async () => {
const { createSectionTitleResolverForLocale } = await import("./section-title-locale");
const resolver = await createSectionTitleResolverForLocale("en-US");
const title = resolver({ sectionId: "experience", locale: "en-US", sectionKind: "builtin" });
expect(typeof title).toBe("string");
expect(title.length).toBeGreaterThan(0);
});
it("caches resolvers per requested locale", async () => {
const { createSectionTitleResolverForLocale } = await import("./section-title-locale");
const a = await createSectionTitleResolverForLocale("en-US");
const b = await createSectionTitleResolverForLocale("en-US");
expect(a).toBe(b);
});
it("falls back to en-US for an unknown locale", async () => {
const { createSectionTitleResolverForLocale } = await import("./section-title-locale");
const resolver = await createSectionTitleResolverForLocale("xx-YY");
const title = resolver({ sectionId: "skills", locale: "en-US", sectionKind: "builtin" });
expect(typeof title).toBe("string");
expect(title.length).toBeGreaterThan(0);
});
});
describe("useSectionTitleResolver", () => {
it("returns null when no locale is provided", async () => {
const { useSectionTitleResolver } = await import("./section-title-locale");
const { result } = renderHook(() => useSectionTitleResolver(undefined));
expect(result.current).toBeNull();
});
it("loads a resolver when a locale is provided", async () => {
const { useSectionTitleResolver } = await import("./section-title-locale");
const { result, rerender } = renderHook(
({ locale }: { locale: string | undefined }) => useSectionTitleResolver(locale),
{
initialProps: { locale: "en-US" as string | undefined },
},
);
// Initially null while the resolver is loading async.
expect(result.current).toBeNull();
await act(async () => {
// Flush microtasks so the async resolver settles.
await Promise.resolve();
await Promise.resolve();
});
expect(typeof result.current).toBe("function");
// Switching to undefined clears the resolver.
rerender({ locale: undefined });
expect(result.current).toBeNull();
});
});
@@ -0,0 +1,122 @@
import type { MessageDescriptor } from "@lingui/core";
import { describe, expect, it, vi } from "vitest";
import { createSectionTitleResolver } from "./section-title";
const makeTranslator = (translate: (d: MessageDescriptor) => string = (d) => d.message ?? "") => ({
_: vi.fn(translate),
});
describe("createSectionTitleResolver", () => {
it("returns the translated message for a built-in section", () => {
const translator = makeTranslator((d) => (d.message === "Experience" ? "Erfahrung" : (d.message ?? "")));
const resolve = createSectionTitleResolver(translator);
const result = resolve({
sectionId: "experience",
locale: "en-US",
sectionKind: "builtin",
defaultEnglishTitle: "Experience",
});
expect(result).toBe("Erfahrung");
expect(translator._).toHaveBeenCalledTimes(1);
});
it("returns the translated message for a custom section by its type", () => {
const translator = makeTranslator((d) => (d.message === "Cover Letter" ? "Anschreiben" : (d.message ?? "")));
const resolve = createSectionTitleResolver(translator);
const result = resolve({
sectionId: "custom-1",
locale: "en-US",
sectionKind: "custom",
customSectionType: "cover-letter",
defaultEnglishTitle: "Cover Letter",
});
expect(result).toBe("Anschreiben");
});
it("falls back to defaultEnglishTitle when the section type is unknown", () => {
const translator = makeTranslator();
const resolve = createSectionTitleResolver(translator);
const result = resolve({
sectionId: "unknown-section",
locale: "en-US",
sectionKind: "builtin",
defaultEnglishTitle: "Fallback Title",
});
expect(result).toBe("Fallback Title");
});
it("falls back to the sectionId when neither title nor known type", () => {
const translator = makeTranslator();
const resolve = createSectionTitleResolver(translator);
const result = resolve({
sectionId: "mystery",
locale: "en-US",
sectionKind: "builtin",
});
expect(result).toBe("mystery");
});
it("falls back to defaultEnglishTitle when translator returns empty string", () => {
const translator = {
_: vi.fn(() => ""),
};
const resolve = createSectionTitleResolver(translator);
const result = resolve({
sectionId: "skills",
locale: "en-US",
sectionKind: "builtin",
defaultEnglishTitle: "Habilidades",
});
expect(result).toBe("Habilidades");
});
it("falls back to sectionId when translator returns empty and no default given", () => {
const translator = { _: vi.fn(() => "") };
const resolve = createSectionTitleResolver(translator);
const result = resolve({
sectionId: "languages",
locale: "en-US",
sectionKind: "builtin",
});
expect(result).toBe("languages");
});
it("resolves all known built-in section ids without errors", () => {
const translator = makeTranslator();
const resolve = createSectionTitleResolver(translator);
const ids = [
"summary",
"profiles",
"experience",
"education",
"projects",
"skills",
"languages",
"interests",
"awards",
"certifications",
"publications",
"volunteer",
"references",
] as const;
for (const sectionId of ids) {
const result = resolve({ sectionId, locale: "en-US", sectionKind: "builtin" });
expect(typeof result).toBe("string");
expect(result.length).toBeGreaterThan(0);
}
});
});
+64
View File
@@ -0,0 +1,64 @@
// @vitest-environment happy-dom
import { beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { isValidElement } from "react";
import { getSectionIcon, getSectionTitle, leftSidebarSections, rightSidebarSections } from "./section";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const ALL_SECTIONS = [...leftSidebarSections, ...rightSidebarSections, "cover-letter"] as const;
describe("getSectionTitle", () => {
it("returns a non-empty string for every known sidebar section", () => {
for (const section of ALL_SECTIONS) {
const title = getSectionTitle(section);
expect(typeof title, section).toBe("string");
expect(title.length, section).toBeGreaterThan(0);
}
});
it("returns distinct titles for each section", () => {
const titles = ALL_SECTIONS.map((section) => getSectionTitle(section));
expect(new Set(titles).size).toBe(titles.length);
});
});
describe("getSectionIcon", () => {
it("returns a React element for every known sidebar section", () => {
for (const section of ALL_SECTIONS) {
const icon = getSectionIcon(section);
expect(isValidElement(icon), section).toBe(true);
}
});
it("forwards icon props such as className", () => {
const icon = getSectionIcon("skills", { className: "custom-class" });
const props = (icon as React.ReactElement).props as { className?: string };
expect(props.className).toContain("custom-class");
// Always merges the shrink-0 baseline class.
expect(props.className).toContain("shrink-0");
});
});
describe("sidebar section collections", () => {
it("expose the documented left-sidebar set", () => {
expect(leftSidebarSections).toContain("picture");
expect(leftSidebarSections).toContain("basics");
expect(leftSidebarSections).toContain("experience");
expect(leftSidebarSections).toContain("custom");
});
it("expose the documented right-sidebar set", () => {
expect(rightSidebarSections).toContain("template");
expect(rightSidebarSections).toContain("design");
expect(rightSidebarSections).toContain("export");
});
it("do not overlap (every section belongs to exactly one sidebar)", () => {
const overlap = leftSidebarSections.filter((s) => (rightSidebarSections as readonly string[]).includes(s));
expect(overlap).toEqual([]);
});
});
@@ -0,0 +1,59 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
vi.stubGlobal("__APP_VERSION__", "9.9.9");
// The footer module evaluates `socialLinks = [{ label: t`...`, ... }]` at module
// scope. That `t` call needs an activated locale BEFORE the import, so do that
// here instead of in beforeAll.
i18n.loadAndActivate({ locale: "en", messages: {} });
const { Footer } = await import("./footer");
const renderFooter = () =>
render(
<I18nProvider i18n={i18n}>
<Footer />
</I18nProvider>,
);
describe("Footer", () => {
it("renders Resources and Community link group headings", () => {
renderFooter();
expect(screen.getByText("Resources")).toBeInTheDocument();
expect(screen.getByText("Community")).toBeInTheDocument();
});
it("renders the documented resource links", () => {
const { container } = renderFooter();
const text = container.textContent ?? "";
for (const label of ["Documentation", "Sponsorships", "Source Code", "Changelog"]) {
expect(text, label).toContain(label);
}
});
it("renders the documented community links", () => {
const { container } = renderFooter();
const text = container.textContent ?? "";
for (const label of ["Report an issue", "Translations", "Subreddit", "Discord"]) {
expect(text, label).toContain(label);
}
});
it("renders social media icon links to GitHub, LinkedIn, and X", () => {
const { container } = renderFooter();
const hrefs = Array.from(container.querySelectorAll<HTMLAnchorElement>("a")).map((a) => a.href);
expect(hrefs.some((h) => h.includes("github.com/amruthpillai/reactive-resume"))).toBe(true);
expect(hrefs.some((h) => h.includes("linkedin.com/in/amruthpillai"))).toBe(true);
expect(hrefs.some((h) => h.includes("x.com/KingOKings"))).toBe(true);
});
it("includes Reactive Resume version copy via Copyright", () => {
renderFooter();
expect(screen.getByText(/v9\.9\.9/)).toBeInTheDocument();
});
});
@@ -0,0 +1,61 @@
// @vitest-environment happy-dom
import { render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
vi.mock("@tanstack/react-router", () => ({
Link: ({ children, to, ...rest }: React.PropsWithChildren<{ to: string }>) => (
<a href={typeof to === "string" ? to : "#"} {...rest}>
{children}
</a>
),
}));
vi.mock("@/components/input/github-stars-button", () => ({
GithubStarsButton: () => <div data-testid="github-stars-button" />,
}));
vi.mock("@/components/locale/combobox", () => ({
LocaleCombobox: ({ render: renderProp }: { render: React.ReactElement }) => renderProp,
}));
vi.mock("@/components/theme/toggle-button", () => ({
ThemeToggleButton: () => <button type="button" data-testid="theme-toggle" />,
}));
i18n.loadAndActivate({ locale: "en", messages: {} });
const { Header } = await import("./header");
const renderHeader = () =>
render(
<I18nProvider i18n={i18n}>
<Header />
</I18nProvider>,
);
describe("Header", () => {
it("renders a homepage link with the brand icon", () => {
const { container } = renderHeader();
const home = Array.from(container.querySelectorAll("a")).find((a) => a.getAttribute("href") === "/");
expect(home).toBeDefined();
expect(home?.getAttribute("aria-label")).toBe("Reactive Resume - Go to homepage");
});
it("renders a dashboard link with the documented aria-label", () => {
const { container } = renderHeader();
const dashboard = Array.from(container.querySelectorAll("a")).find((a) => a.getAttribute("href") === "/dashboard");
expect(dashboard).toBeDefined();
});
it("includes ThemeToggleButton and GithubStarsButton in the navigation", () => {
const { getByTestId } = renderHeader();
expect(getByTestId("theme-toggle")).toBeInTheDocument();
expect(getByTestId("github-stars-button")).toBeInTheDocument();
});
it("labels the navigation landmark", () => {
const { container } = renderHeader();
const nav = container.querySelector("nav") as HTMLElement;
expect(nav.getAttribute("aria-label")).toBe("Main navigation");
});
});
@@ -0,0 +1,35 @@
// @vitest-environment happy-dom
import { render, screen } from "@testing-library/react";
import { beforeAll, describe, expect, it } from "vitest";
import { i18n } from "@lingui/core";
import { I18nProvider } from "@lingui/react";
import { Prefooter } from "./prefooter";
beforeAll(() => {
i18n.loadAndActivate({ locale: "en", messages: {} });
});
const renderPrefooter = () =>
render(
<I18nProvider i18n={i18n}>
<Prefooter />
</I18nProvider>,
);
describe("Prefooter", () => {
it("renders the community tagline as a heading", () => {
renderPrefooter();
expect(screen.getByText("By the community, for the community.")).toBeInTheDocument();
});
it("renders the community-thanks paragraph", () => {
renderPrefooter();
expect(screen.getByText(/vibrant community/)).toBeInTheDocument();
});
it("renders the decorative TextMaskEffect (svg)", () => {
const { container } = renderPrefooter();
expect(container.querySelector("svg")).not.toBeNull();
});
});
@@ -0,0 +1,28 @@
import { afterEach, describe, expect, it } from "vitest";
import { useBuilderAssistantStore } from "./assistant-store";
afterEach(() => useBuilderAssistantStore.setState({ isOpen: false }));
describe("useBuilderAssistantStore", () => {
it("starts closed", () => {
expect(useBuilderAssistantStore.getState().isOpen).toBe(false);
});
it("setOpen overrides the open state directly", () => {
useBuilderAssistantStore.getState().setOpen(true);
expect(useBuilderAssistantStore.getState().isOpen).toBe(true);
useBuilderAssistantStore.getState().setOpen(false);
expect(useBuilderAssistantStore.getState().isOpen).toBe(false);
});
it("toggleOpen flips the state", () => {
const { toggleOpen } = useBuilderAssistantStore.getState();
toggleOpen();
expect(useBuilderAssistantStore.getState().isOpen).toBe(true);
toggleOpen();
expect(useBuilderAssistantStore.getState().isOpen).toBe(false);
});
});

Some files were not shown because too many files have changed in this diff Show More