Add multi-language CV variants with language switcher#111
Merged
Conversation
Users can now save localized versions of each CV (e.g. English + German)
that share the same structure but have independent content. Language
variants are linked via a language_group and share the same URL slug.
Key changes:
- Database: language + language_group columns on saved_datasets with
composite unique constraints (slug,language) and (name,language)
- Structural propagation: section order, visibility, and custom section
layout sync automatically across language siblings on save
- Public: language switcher at /v/{slug}/{lang}, /?lang=xx for root
- Admin: functional language switcher in dataset banner, language
dropdown in Save As modal, language badges in Open modal
- Group-wide default/public toggles apply to all language variants
- i18n keys added to all 8 locale files
Bumps version to 1.28.0.
https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
…ll redesign 1. Switching dataset language now also changes the UI locale (headers, sections, menus) to match — both in admin and public mode. 2. Toolbar language picker greys out languages not in the active dataset's language group with a hint to use Save As for new ones. Clicking an available language in the toolbar switches to that dataset variant (equivalent to the banner switcher). 3. Public language pill moved to top-right corner with country flag emoji next to the ISO code instead of the translate icon. Dropdown also shows flags per language. 4. "Add language" button moved to sub-version level (each dataset row) instead of parent group level in the Save As modal. 5. Added flag emoji property to I18n.languages entries. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
1. Collapsible version groups: In both Open and Save As modals, only the latest version is shown when a dataset has multiple versions. Older versions are hidden behind an expandable "N older versions" toggle that can be clicked to reveal them. 2. Per-language public visibility: The public/shared toggle now applies to individual language variants instead of the entire group. Users can choose exactly which languages to make publicly accessible on the public site. New language variants start private by default. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
1. Three-level grouping: Datasets are now displayed as Dataset → Version → Language in both Open and Save As modals. Languages appear nested under each version. The "Add language" button sits at version level (or dataset level if single version). 2. Remove flag emojis from I18n.languages — reverted to plain text. Public language pill now uses round flagcdn.com images (same source as timeline country flags) with border-radius: 50% for a circular appearance. Language-to-country-code mapping added to scripts.js. 3. Save As modal: Cancel and Save/Overwrite buttons moved inline next to the name input and language dropdown for a more intuitive layout. The modal footer is removed. 4. Older versions remain collapsible behind an "N older versions" toggle in both modals. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
1. Default CV now serves the correct language variant when multiple
languages are available. Added /:lang([a-z]{2}) route on the public
server so visitors can access /en, /de, /fr etc. directly.
2. Open modal: default datasets with multiple languages now show
/{lang} URLs instead of hiding the URL. Non-default datasets
continue to show /v/{slug}/{lang}.
3. Public language pill: uses the same styling as the print button
(primary background, white text, matching shadow). Round flagcdn
country flag images displayed both in the pill and dropdown items.
Active dropdown item uses primary background with white text.
4. Language switcher URLs for default datasets use clean /{lang} paths
instead of /?lang=xx. Server injects DATASET_IS_DEFAULT flag so
the client-side switcher builds the correct URLs.
https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
1. Admin now persists the active dataset ID in sessionStorage, so refreshing the page restores the exact dataset+language variant the user was editing — not just the first default variant. This also fixes the public site showing the wrong language after an admin edit, since the correct variant is now consistently loaded. 2. Fixed round flag images: flagcdn only serves specific widths (w20, w40, w80), so always fetch w40 and let CSS scale to the desired display size with border-radius: 50%. 3. persistActiveDataset() is called on every state change: loadDataset, submitSaveAs, switchDatasetLanguage, hideActiveDatasetBanner. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
The root URL / now serves whichever default language variant was most
recently updated (by updated_at timestamp), instead of always
preferring English. This means when the admin edits and saves the
German variant, visitors at / immediately see the German CV.
The same logic applies to /v/{slug} when no language is specified —
it now serves the most recently updated variant rather than defaulting
to English.
Visitors can still explicitly request a language via /{lang} URLs
(e.g. /en, /de) or /v/{slug}/{lang}.
https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
The default radio button in the Open modal now sets exactly ONE
specific dataset variant as the default served at /. Previously it
marked the entire language group as default.
How it works now:
- User clicks the radio on "My CV v2 (FR)" → only that row gets
is_default = 1, all others become 0
- Root / serves that French v2 variant
- Language siblings that are marked public appear in the language
switcher so visitors can switch to /en, /de etc.
- New language variants are NOT auto-set as default
The public root / now finds THE one default variant. When a visitor
requests /{lang}, it looks for a public sibling in the same group
with that language. If not found, falls back to the default.
https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
When clicking "New version" on a dataset that has multiple languages (e.g. EN + DE), the backend now copies all language siblings from the source version into the new version's language group. The primary language uses the current live data; other languages copy their data from the source group. Frontend passes source_group (the source version's language_group UUID) when clicking "New version". The backend uses this to find and copy sibling language variants. This means renaming doesn't affect language carry-over — it uses the UUID-based language_group, not the parsed name convention. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Replaces the fragile name-parsing approach ("Name vN" regex) with
explicit database-backed version tracking. Two new columns on
saved_datasets:
- version_group (TEXT UUID): links all versions of the same dataset
- version (INTEGER): the version number within the group
Migration (Step 2p) assigns version_group and version to existing
datasets by parsing their names, so same base names get the same
version_group UUID. The UNIQUE constraint changes from (slug, language)
to (slug, version, language) so versions can share slugs.
Backend: POST /api/datasets accepts version_group param. When provided,
auto-increments version, copies slug, and carries over all language
siblings from the previous version. All endpoints return version_group
and version in responses.
Frontend: groupDatasetsHierarchy now groups by version_group UUID
from the API instead of regex name parsing. suggestNextVersion uses
the version field. Renaming a dataset no longer breaks version grouping.
4 new backend tests covering version increment, language sibling
carry-over, slug sharing, and version fields in responses.
Bumps version to 1.29.0.
https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Root cause: the slug+language query was ambiguous when multiple versions share the same slug. Selecting "French v2" as default set is_default=1 on that row, but the public site loaded data by slug+lang which could match v1 French instead. Fix: the server now injects window.DATASET_ID (the exact row ID) for the default dataset. The public-readonly client uses /api/datasets/id/:id to fetch data by exact ID, guaranteeing the correct version+language. Also injects DATASET_ID for /v/:slug/:lang and admin preview pages. The slug+lang API remains as fallback for non-default datasets. New endpoint: GET /api/datasets/id/:id on both public server instances (filters by is_public=1 OR is_default=1 for security). https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Root cause: language siblings of the default dataset were not accessible on the public site because they had is_public=0 (new variants start private). The sibling lookup in servePublicIndex required is_public=1, so clicking a language in the switcher fell back to the default language instead of switching. Three fixes: 1. servePublicIndex: when looking up a language sibling of the default, no longer requires is_public=1. All siblings in the default's language_group are accessible for language switching. 2. getDatasetSiblings: when the dataset is default, returns ALL siblings in the group (not just public ones), so the language switcher pill shows all available languages. 3. serveDatasetDataById: allows API access to non-public datasets if they share a language_group with a default dataset. Verified with end-to-end test: setting FR as default with EN not public → / serves FR, /en correctly serves EN, language switcher shows both languages, API serves both by ID. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Root cause: getDatasetSiblings checked dataset.is_default to decide whether to include all siblings. But only the ONE default row (e.g. DE) has is_default=1. When a visitor navigated to /en, the EN row was served — it has is_default=0, so getDatasetSiblings took the restricted path and excluded FR (is_public=0, is_default=0). Fix: getDatasetSiblings now checks if ANY member of the language_group has is_default=1 (not just the passed dataset). If so, ALL siblings in the group are returned regardless of is_public status. This ensures the language switcher shows all available languages on every page. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Active language option now uses white text on blue background for proper contrast. The "Add language" item is no longer greyed out — clicking it opens the Save As modal pre-filled for adding a new language variant to the current language group. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
…auto-expand - Remove border-radius from dataset items, groups, and Save As items for straight left-side borders - Add version header rows to the Open modal (badge + name) matching the Save As modal's version→language nesting pattern - Auto-expand collapsed older versions when they contain the active or default dataset so the user can always see them https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
When a dataset has no versions and no language variants, render it as a flat clickable row instead of a version header + tree-connected child, which duplicated the name and looked confusing. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Both "New version" buttons in the Save As modal were passing language_group instead of version_group. This caused new versions to get a fresh version_group UUID instead of inheriting the original's, so they appeared ungrouped in the list. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Datasets created via the broken "New version" button ended up with different version_group UUIDs despite sharing the same base name. On startup, detect datasets whose base names match but have different version_groups, and consolidate them under the canonical (earliest) version_group. This runs every startup but is a no-op when data is already consistent. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Export now adds the active dataset language to the JSON and names the
file cv-data-{lang}.json. Import reads the language field back, switches
the UI locale to match, and sets activeDatasetLanguage so subsequent
Save As picks up the correct language.
https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
The Open modal now shows a clickable language badge on every dataset row. Clicking it opens a floating picker listing all supported languages. Selecting one PUTs /api/datasets/:id/language, which validates the change against name/slug/version/language_group uniqueness constraints and rejects conflicts with a 409. This lets users relabel legacy datasets whose language was auto-defaulted to 'en' during migration, without a blind migration that could mislabel intentionally-English datasets. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
When a version has only one language, skip the intermediate version header + tree-connected child and render the dataset row directly with the version badge inline. This eliminates the redundant three- level nesting (group name → version name → dataset name) that repeated the same name three times. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
The "Last updated" text now sits on the same line as the dataset name, pushed to the right with margin-left: auto. This balances the row layout and reduces vertical space. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
In the Open modal, move the date out of dataset-info into its own element between info and actions so it sits to the far right of the row. In the Save As modal, apply margin-left:auto to save-as-row-date universally and restructure the flat row so the date is a direct flex child pushed right. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
The flat row (single-language) and single-language tree child were missing the language badge. Now every row in the Save As modal shows the language code, matching the Open modal's behavior. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Save As modal now auto-expands collapsed older versions when one contains the dataset being edited, matching the Open modal behavior. Languages within each version now sort by most recently modified instead of alphabetically, so the most active variant appears first in both modals. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
When a dataset is the default, its language siblings in the same
language_group are now treated as implicitly public: the private/shared
toggle is hidden and the URL shows /?lang={code} instead of
/v/{slug}/{lang}. The left border also highlights them as default
siblings. This matches the backend behavior where siblings of the
default are always accessible without needing is_public.
https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
The Add Language button now appears on single-language flat rows in the Save As modal, so users can always add language variants regardless of the dataset structure. The New Version button moves from the group footer into the group header, placed between the base name and the version count indicator. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Replace the two separate dataset modals with one unified "CV Manager" modal. The toolbar now has a single "CV Manager" button that opens a modal with: - Top zone: save-as form (name + language + submit) - Bottom zone: full dataset list with Load + overflow menu per row Each row renders flat: [radio] [lang] [version] name ... date [Load] [⋮] The ⋮ menu contains: share/private, change language, preview, copy URL, delete. No tree nesting for single-language versions. Group headers show: base name + New Version button + version count. Compact legend moved to the footer. Old modal code removed. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
…d visual grouping - Hide native radio input inside .cvm-radio (was rendering both native and styled dot) - Switch UI locale and reload editor after saving a language variant - Add New Version button to single-version multi-language group headers - Indent language rows inside groups and remove individual left borders (only show active/default highlights) https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Single-version datasets now render with the same group header + body structure as multi-version ones: base name, New Version button, Add Language button, and a count badge (1 VERSION or N LANGUAGES). This eliminates the visual inconsistency where standalone datasets had a different layout. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
When the active locale is not English, the ATS PDF modal shows a checkbox to optionally render section headers (Work Experience, Education, etc.) in English while keeping everything else in the active language. Useful for international CVs targeting English- speaking ATS systems. The toggle is hidden when the locale is already English. The server uses a separate tHeader() translator scoped to 'en' for section names, while dates and other text stay in the active locale. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Rewrite datasets.md for unified CV Manager modal, language variants, version management, overflow menu, and default siblings. Expand language.md with CV content language, language variants, and public site switching. Update import-export.md with language in exports. Add ATS English headers toggle to ats.md. Add language variant FAQs to faq.md. Update guide index descriptions. Translations in progress — will follow in separate commit. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Deploying cv-manager with
|
| Latest commit: |
f22a647
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://beda9044.cv-manager-a5e.pages.dev |
| Branch Preview URL: | https://claude-add-cv-localization-q.cv-manager-a5e.pages.dev |
- Fix preview passing dataset name instead of slug to previewDataset() - Fix copy URL using name instead of slug - Show share icon inline on shared dataset rows (visible without opening the overflow menu) - Make legend more explicit: "Served on public site" and "Shared at /v/ URL" instead of just "Default" and "Shared" https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Translated datasets.md, language.md, import-export.md, ats.md, and faq.md to de, fr, nl, es, it, pt, zh. Some index files also updated. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
The /api/datasets/id/:id and /api/datasets/slug/:slug routes were only registered on the public server. When the admin previewed a dataset at /v/slug, the page fetched the data API from the admin server which didn't have the route, returning HTML instead of JSON. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
The admin server's dataset data routes were reusing the public server's functions which require is_public or is_default. Admin preview of private datasets returned 404. Now the admin routes query without visibility filters — admin can preview any dataset. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
The icon is now inside .cvm-name so it appears right after the text, before flex: 1 pushes the remaining elements to the right. https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
Green tinted background and border, same dimensions as the version badge (min-width 26px, radius-sm, 1px border). https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR adds support for creating and managing language-specific variants of CVs. Users can now save multiple language versions of the same CV (e.g., English + German) and switch between them seamlessly. Language variants share a common slug and are grouped together, with the ability to switch languages both in the admin interface and on public CV pages.
Key Features
languageandlanguage_groupfieldslanguageandlanguage_groupcolumns tosaved_datasetstable with proper UNIQUE constraintsTechnical Changes
saved_datasetstable now haslanguage(default 'en') andlanguage_group(UUID) fieldsGET /api/datasets/:id/siblingsto retrieve language variantsType of Change
Checklist
Required for all code changes
npm testpasses)package.json,package-lock.json,version.json) — bumped to 1.28.0CHANGELOG.mdhas been updated with a new entry under version 1.28.0If adding or changing user-visible strings
t('key')in JS ordata-i18nin HTMLen.jsonand all 7 other locale files (de,fr,nl,es,it,pt,zh)escapeHtml()used for any user-provided content rendered as HTMLTest Coverage
https://claude.ai/code/session_01CnDenkpciR4WdBHQGMDuGX