mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-06-22 04:11:55 +10:00
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:
+1
-1
@@ -98,4 +98,4 @@ GOOGLE_CLOUD_API_KEY=""
|
||||
# Crowdin (optional)
|
||||
# For translation tooling.
|
||||
CROWDIN_PROJECT_ID=""
|
||||
CROWDIN_API_TOKEN=""
|
||||
CROWDIN_PERSONAL_TOKEN=""
|
||||
|
||||
@@ -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:**
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 URL‑i 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 l’URL 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 URL‑t:"
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 open‑source 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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 bo‘lib, 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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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
Reference in New Issue
Block a user