[variant — do not merge] Document-level translation (alternative to field-level i18n)#62
Draft
damianrosellen1 wants to merge 49 commits into
Draft
[variant — do not merge] Document-level translation (alternative to field-level i18n)#62damianrosellen1 wants to merge 49 commits into
damianrosellen1 wants to merge 49 commits into
Conversation
- Install @sanity/document-internationalization (^6.2.1).
- studio/sanity.config.ts: replace internationalizedArray({...}) with
documentInternationalization({ supportedLanguages, schemaTypes,
languageField }).
- studio/config/sync/internationalizedArrayLanguages.ts renamed to
supportedLanguages.ts (loader signature identical, plus a try/catch
fallback that addresses the earlier robustness gap).
- All 9 schemas migrated: page, home, errorSettings, siteSettings,
siteNav, siteCookieBanner gain a hidden `language` field; module
objects (text/carousel/contentRefs) get plain string/richText/
richTextMedia field types (no per-field i18n wrapper). Previews use
plain string access; firstLocalizedLabel helper deleted.
NOTE: this is an intermediate commit on the long-lived variant branch.
typecheck/build are intentionally NOT green here — the queries, web
types, fetch wrappers, components, structure items, and presentation
resolver still assume field-level i18n and will be migrated in the
following commits.
Studio side of the document-level migration is now internally consistent
(studio typecheck green; web side migrates in the next etappe).
Structure items for the five singletons (home, siteSettings, siteNav,
errorSettings, siteCookieBanner) switch from a fixed-id S.document() to
S.documentTypeList(<type>) so the desk shows one row per language
variant of each singleton; the plugin's Translations toolbar handles
sibling switching inside each document.
Slug uniqueness is now language-aware: `studio/utils/validateSlug.ts`
exports `isUniqueLocaleAgnostic` which scopes uniqueness per (_type,
language). `studio/schemas/documents/page.ts` wires it into the slug
field's `options.isUnique`, so /about can exist in en AND de.
Presentation:
- `resolve.ts` `presentationMainDocuments` now registers four routes:
`/` and `/:slug` (default-locale, no language constraint), plus
`/:locale` and `/:locale/:slug` (`language == $locale`). Presentation
prefers the most specific match, so per-locale routes win when the
iframe URL carries a locale segment.
- `locationsResolver.ts` SLUG_QUERY now selects `language` alongside
`slug.current` and emits a `/{language}/{slug}` URL. The web proxy
redirects default-locale prefixes to the canonical unprefixed URL.
schema.json and sanity.types.gen.ts regenerated under the new
schemas (52 types incl. `translation.metadata` from the plugin).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Web is now consistent with the Studio's document-level model:
QUERIES
- `pages/home.ts` and `pages/page.ts` filter by `language == $locale`
and project plain `title` (no field-level array wrapper). Both wrap
in `defineQuery` — the typegen-blocking `richTextMediaQuery` recursion
is gone, so the full pipeline can be typed now.
- `snippets/settings.ts`: every settings/nav query takes `$locale` and
filters at the document level; `internationalizedRichTextArrayField`
helper deleted; error-body fields project plain `richText` blocks.
- `snippets/sitemap.ts`: rows carry the document's `language` so each
language variant is emitted as a separate URL.
- `components/text/richTextMedia.ts`: rewrite from the recursive
`buildRichTextMediaQuery(depth)` function to a static depth-2 inline
literal (typegen-friendly, idempotent).
- `components/modules/text.ts`: flatten body — no `{language, value}`
wrapper.
TYPES
- `web/sanity/utils/sanityLocalizedText.ts` deleted; the utils barrel
drops its re-exports of `parseLocalizedText`, `pickLocalizedString`,
`pickLocalizedPortableTextBlocks`, `resolveLocalizedPortableTextDeep`
and the `Intl*Entry` shapes.
- Hand-written types in `web/sanity/types/{pages,errorSettings,modules/*}`
switch from `Intl*Entry[]` to plain `string`/`PortableTextBlock[]`,
and gain a top-level `language` field where the doc carries one.
- `web/sanity/sanity.types.gen.ts` regenerates with 7 typed queries
(HomeQueryResult and PageBySlugQueryResult now resolve cleanly — the
full-pipeline bonus promised in the plan).
FETCH WRAPPERS / ROUTES
- `fetchSanityData.ts`: every per-document wrapper takes `locale: string`
(`fetchHomeDocument`, `fetchPageBySlug`, `fetchErrorSettings`,
`fetchSiteSettingsTitle`, `fetchSettingsSeoFallback`, `fetchSiteNavMenus`).
`fetchSiteLanguageSettings` stays unparameterised — it is the locale
registry itself.
- `cachedSanityQuery.ts`: `cachedPageDocumentBySlug(slug, locale)` and
`cachedHomeDocument(locale)`; cache keys/tags become
`page-{slug}-{locale}` / `home-{locale}`.
- `/api/revalidate/route.ts`: payload validation accepts `language`; the
emitted tags include it for `home` and `page`.
- Route pages (`[locale]/page.tsx`, `[locale]/[slug]/page.tsx`,
`not-found.tsx`, `sitemap.ts`) thread `locale` through every fetch.
- `generateStaticParams` for `[slug]` now uses each page document's
declared `language` instead of the previous cartesian locale × slug
product — only the slugs that actually exist for a language are pre-
rendered.
COMPONENTS
- `ModuleText`, `ModulesRenderer`, `LocaleNotFoundContent`,
`resolveSanityMetadata` access `module.title`/`data.title` directly;
no more `pickLocalized*` calls. The `siteLocale` prop drops off the
module renderer chain.
VERIFICATION
- `pnpm -r run typecheck`: green on studio AND web.
- `pnpm --filter studio run generate` + `--filter web run generate`:
both idempotent against the committed artifacts.
- `pnpm --filter studio run build` and `pnpm --filter web run build`:
green.
- `pnpm lint`: clean (only the pre-existing `useOptionalChain` warning
on `RichTextMedia.tsx`).
The branch is now functionally complete. Issue #18 remains open as
the tracking issue for the variant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 28, 2026
… resolution Updated the dataset resolution logic to exclusively use SANITY_STUDIO_PROJECT_ID, removing the fallback to NEXT_PUBLIC_SANITY_PROJECT_ID in both the resolveStudioDatasetAsync function and getSanityStudioProjectId function. This change simplifies the project ID retrieval process.
Consolidated the project ID retrieval logic in both the resolveStudioDatasetAsync and getSanityStudioProjectId functions by removing unnecessary line breaks. This enhances code readability without altering functionality.
…dability Streamlined the JSX structure in the MediaImage component by eliminating redundant line breaks, enhancing code clarity without affecting functionality.
✅ Deploy Preview for next-sanity-boilerplate-test ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…mbs, autoplay) Adds CMS-driven carousel behavior fields (loop, showThumbnails, showNavDots, autoplay, autoplayDelayMs) and a production ModuleCarousel renderer using embla-carousel-react + embla-carousel-autoplay. Replaces the dev-only placeholder in ModulesRenderer and supports embeds in RichTextMedia. Co-authored-by: Cursor <cursoragent@cursor.com>
…hydration mismatch The boot script synchronously added `.img-loaded` to images already complete in the browser cache, racing React 19 streaming hydration and producing a "tree hydrated but attributes didn't match" warning on every page that ships a non-priority Sanity image. Wrapping the class mutation in `requestAnimationFrame` lets React reconcile the SSR markup first while still keeping the fade-in transition imperceptible. Co-authored-by: Cursor <cursoragent@cursor.com>
…o fix hydration `syncSanityProjectId` previously read `SANITY_STUDIO_PROJECT_ID`, which Next does not inline into the client bundle. SSR therefore built transformed Sanity image URLs (`?w=…&auto=format&q=85`) while the hydrating client had no project id and fell back to bare `image.asset.url`, producing a hydration mismatch on every Sanity image. Read `NEXT_PUBLIC_SANITY_PROJECT_ID` exclusively (and drop the equivalent server-only `SANITY_STUDIO_DATASET` branch from `syncDataset`) so server and client always compute identical URLs. Document the new env var requirement in `.env.example`. Co-authored-by: Cursor <cursoragent@cursor.com>
…mismatch Adding the `img-loaded` class from the inline boot script raced React 19's streaming hydration on cached images and produced "tree hydrated but attributes didn't match" warnings. `requestAnimationFrame` was not enough to push the DOM mutation past hydration reliably. Move the lazy fade-in inside `MediaImage`: a `useRef` + `useEffect` flips a `loaded` state once the image's `load` event fires (or immediately if the image is already complete) and React renders the class — no more DOM / hydration race. Strip the image-handling section from the boot script — theme + `js-enabled` stay so the CSS opacity gate keeps working. Co-authored-by: Cursor <cursoragent@cursor.com>
Updated `siteNavQuery` and `siteNavMenusQuery` to utilize a new `siteNavByLocale` query, which retrieves the active locale's `siteNav` document while falling back to a legacy document if no locale-specific document exists. This change improves internationalization handling in the navigation structure.
…rth/next-sanity-boilerplate into variant/document-level
`web/.env` previously needed both `SANITY_STUDIO_PROJECT_ID` (server / Studio convention) and `NEXT_PUBLIC_SANITY_PROJECT_ID` (so Client Components could build matching Sanity image URLs). Same value, two names — confusing and error-prone (forgetting one half of the pair causes a hydration mismatch). Use Next's `next.config.ts` `env` map to inline `SANITY_STUDIO_PROJECT_ID` and `SANITY_STUDIO_DATASET` into the client bundle. `sanitySyncConfig.ts` now reads the Studio-named vars directly. `.env.example` drops the public aliases. One env var, one name, identical SSR / CSR Sanity URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
… at opacity:0 `useEffect` only set `loaded=true` synchronously when both `el.complete` and `naturalWidth > 0` were true. For images that errored before React's passive effect attached its listeners, the load/error events had already fired and were lost — `loaded` never flipped, the `img-loaded` class was never added and the broken image stayed invisible (opacity:0) forever. Drop the `naturalWidth > 0` guard. Both successful and failed loads need to reveal the `<img>` (the alt text matters for failed ones); the running listeners still cover the in-flight case. Co-authored-by: Cursor <cursoragent@cursor.com>
…a-sanity
Add a `dataAttr` helper that composes `next-sanity`'s `createDataAttribute`
with the resolved `projectId` / `dataset` and the Studio URL, so callers only
need to pass the field path (`{ id, type, path }`). Used in `ModulesRenderer`
to wrap each module with `<div data-sanity={...}>` — Presentation tool can
now reverse-map a rendered module to its `modules[_key=="…"]` GROQ slot and
jump the Studio cursor into the corresponding field.
Skips the attribute when a module has no `_key` (legacy data) — without a
stable key the overlay would target the wrong array slot.
Co-authored-by: Cursor <cursoragent@cursor.com>
Silent failures of the SanityLive socket made it hard to tell whether live content was syncing during draft mode. Pass an `onError` handler that emits the error plus the request context (`includeDrafts`, `waitFor`) to `console.error` — surfaces issues in DevTools without changing behavior. Co-authored-by: Cursor <cursoragent@cursor.com>
Replaced the inline boot script with a new DocumentBootScript component that sets the theme based on localStorage and adds a 'js-enabled' class to the <html> element. This change enhances the structure and maintainability of the layout while ensuring the theme is applied before the first paint. Updated related comments in animations.css to reflect this change.
…sRenderer - Introduced dynamic import for ModuleCarousel in ModulesRenderer to optimize loading. - Updated RichTextMedia to utilize LinkMark from linkResolver, simplifying link management. - Refactored LinkMark component to improve link resolution and rendering logic. These changes improve performance and maintainability of link handling across components.
…ering Introduced a new component, ModulesRendererClient, which wraps server-rendered modules and manages their reordering based on updates from Sanity Visual Editing. This component optimizes client-side rendering while ensuring that newly added modules appear after a refetch. The implementation enhances the user experience by providing instant feedback for reordering and deletion actions.
…ering Introduced a new component, ModulesRendererClient, which wraps server-rendered modules and manages their reordering based on updates from Sanity Visual Editing. This component optimizes client-side rendering while ensuring that newly added modules appear after a refetch. The implementation enhances the user experience by providing instant feedback for reordering and deletion actions.
`<SanityLive />` is rendered from the server root layout, and Next 16 / Turbopack rejects passing a function literal as a prop across the RSC boundary. Extract `handleSanityLiveError` into a "use client" module so it becomes a client reference the layout can safely import and pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is the document-level translation branch, where each locale is its
own Sanity document. The Presentation overlay should jump editors to the
modules array as a whole rather than into individual field paths.
- dataAttr helper: drop 'path' from the Required pick so doc-level call
sites are expressible at the type level. Runtime still rejects empty
paths, so the shallowest valid scope here is the top-level field name.
- ModulesRenderer: compute one container-scoped data-sanity attribute
('modules' path) and pass it to ModulesRendererClient as a prop instead
of attaching per-module field paths.
- ModulesRendererClient: accept optional containerSanityAttr and apply it
on the outer flex container, leaving the per-module wrappers attribute-
free.
The main (field-level i18n) branch keeps per-module deep paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updated the Header component to conditionally display a language switcher based on the number of available languages. Refactored menu entries to exclude the language switch option when not needed. Improved layout by grouping the language switcher and theme toggle for better UI consistency across mobile and desktop views.
…eader Updated the Header component to enhance the readability of the LanguageSwitch component by formatting its props across multiple lines. This change aims to improve code clarity without altering functionality.
damianrosellen1
added a commit
that referenced
this pull request
May 28, 2026
Restructure and tighten the root README for publication while keeping every existing piece of accurate technical content. What changed: - Stronger lead: one-line tagline + a row of shields.io badges for Next, React, Sanity, TypeScript, Tailwind, Biome, license. No emoji. - New "Two flavours" section right under the lead — surfaces the variant/document-level branch and links to PR #62 (the permanent side-by-side comparison and decision guide). - "What you get" reflows the previous dense bullet sprawl into themed paragraphs (Next.js app, Studio, Media, Revalidation, SEO, Hardened defaults, Typed GROQ, Tailwind v4). Each block reads like a feature card, not a checklist. - Quickstart split into clean steps instead of one bash block with inline numbered comments. - "Architecture in brief" replaces the long "Core APIs & modules" tables; depth moves into the per-folder READMEs (already linked). - New "License" footer; the "Requirements" block moves to the bottom where it belongs (you don't read it before you're sold on the repo). - Fixed an orphan link to `packages/sanity-dataset-resolve/README.md` (file doesn't exist; resolver is documented inline in `src/index.ts`). - The strip-readmes utility gets one explicit mention so consumers know the documentation density is opt-in. Net result: 271 → 213 lines, 0 emoji, all links resolve. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rolls out the public-release README from #63 to this branch, adapting the branch-specific sections for document-level i18n: - "Two flavours" table marks this branch as the current one - Studio description points at the Translations toolbar (@sanity/document-internationalization) instead of per-field language tabs - "Typed GROQ pipeline" notes full typegen coverage on this branch - Get-started flow uses the Translations toolbar + per-locale slugs - "Architecture → i18n" rewritten for document-per-locale + `language == $locale` GROQ filtering (no runtime resolver) Everything else (stack, env, scripts, tooling, deploy, going-deeper) matches the main README verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Makes the light/dark theme toggle an optional, editor-curated navigation element — mirroring the nav.languageSwitch pattern. The toggle renders only where an editor places the new `nav.themeToggle` block in Navigation → Main Menu, via MainMenuItems; the always-on standalone toggles are removed from the header chrome. - New Studio object `nav.themeToggle` (registered + added to siteNav mainMenu) - GROQ snippet, `nav.ts` types, and `navHref` resolver handle the new block - Regenerated schema.json + typegen artifacts Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d drop typography-clamp Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Document-level i18n schemas hid the `language` field with no `initialValue`, so newly created documents (including freshly opened singletons) were stored without a language and disappeared from `language == $locale` queries until an editor set it manually. Set `initialValue: "en"` on the `language` field of every translatable document type: home, page, errorSettings, siteNav, siteSettings, siteCookieBanner. The document-internationalization plugin's "Add translation" flow still overrides this with the target locale; only the bare "Create new" flow benefits.
Wires the Sanity-driven cookie banner into the boilerplate: lightweight layout query + fetcher, client components under web/src/components/cookies, and an `open-cookie-preferences` linkFunction handled in NavItem. The CSS maps `--cc-*` tokens onto the boilerplate's `--color-*` semantic tokens so the banner inherits light/dark mode. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Cookie consent (optional)" entry to the "What you get" section and removes the `letter-spacing` declarations from every `@utility` block in `typography.css` plus the matching three lines in the Studio Portable Text previews. The `--letter-spacing-*` tokens stay in place for callers that still need them; `blockquote p` keeps `letter-spacing: inherit`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renames the repo `next-sanity-boilerplate` → `next-sanity-starter` and updates every in-repo reference: README headings + prose, root `package.json` (name + description), Studio Sanity title, the strip-readmes workflow commit message, the CI build comment, and the inline comments in `web/next.config.ts`, `web/src/app/robots.ts`, and the fonts README. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Centralize Portable Text vertical rhythm under `.rich-text` in typography.css and strip margin utilities from RichTextMedia so spacing stays consistent across rich-text renderers. Fixes #68 Co-authored-by: Cursor <cursoragent@cursor.com>
Switch internal/external/function link titles from a GROQ coalesce on `reference->title` to raw `title` + `resolvedReference.title`, then resolve in app code via `resolveLinkLabel`. Handles both i18n-array nav titles and plain string document titles so siteNav (singleton, field-level i18n) and doc-level page titles share one code path. Adds preliminary work/project routes to linkResolver + navHref. Co-authored-by: Cursor <cursoragent@cursor.com>
Brings the variant/document-level branch in sync with the main branch's recent features (dark-mode toggle, multi-slide carousel, work/projects, module.contentRefs, module structure refactor) while keeping document- level internationalization as the canonical i18n pattern. Highlights: - Colors/typography/rich-text token updates (incl. --color-error fix and rich-text.css for Portable Text spacing) - Toggle-only dark mode (ThemeContext, ThemeToggle, DocumentBootScript) - Multi-slide carousel (schema, queries, types, viewport/slide layout) with arrow + dot controls beneath the viewport - Modules folder refactor: shared moduleStyles, relocated ModuleCarousel into src/components/modules, barrel exports - Work/Project/ProjectCategory document types adapted to document-level i18n (plain `title`, `language` field, locale-filtered GROQ) - `module.contentRefs` adapted for document-level i18n (language-filtered references, plain-string titles/categories) - Plain-string link titles (no internationalizedArray plugin needed on this branch); resolveLinkLabel falls back to referenced doc titles - Studio Presentation routes for /work and /work/[slug] localized via language-aware locationsResolver and main-documents Co-authored-by: Cursor <cursoragent@cursor.com>
Port main updates to variant/document-level
✅ Deploy Preview for bef-next-sanity-starter ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
…specific tweaks) Ports the canonical AGENTS.md + per-subtree CLAUDE.md + Cursor rules + Copilot instructions from main. Branch-specific notes in AGENTS.md already account for the variant's reduced renderer set, contentRefs schema simplification, document-level language field, and the missing modules/index.ts barrel. Variant-only tweak: web/src/components/modules/CLAUDE.md notes that ModulesRenderer imports module components directly and that re-creating an index.ts barrel would be a dead file on this branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ails to variant/document-level Mirrors the additions made on `main`: - @repo/scaffold-module — `pnpm gen:module <Name>`. The components/modules barrel patch is marked optional, so the script gracefully skips it on this branch (no barrel exists; ModulesRenderer imports directly). - @repo/check-wiring — `pnpm check:wiring`. Branch-aware by design: treats missing barrel as a skip and accepts Module<Name>Placeholder fallbacks in ModulesRenderer (covers ModuleContentRefs on this branch). - Strip-readmes: root README.md is preserved. - CI: wiring drift gate after typecheck. - AGENTS.md: full CLI command reference (root/web/studio); CLAUDE.md files trimmed and aligned to Claude Code best practices (concise, @-imports, YOU MUST / IMPORTANT emphasis on the critical rules). - web/src/components/modules/CLAUDE.md: variant-specific note that the generator skips the barrel patch and that ContentRefs is currently a placeholder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-commit hook reformatted after staging. No behavioural change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(tooling): agent guardrails + gen:module + check:wiring (variant)
Dependabot reads its config from the default branch only, so this file on `variant/document-level` is functionally ignored — but keeping it diverged from main causes merge conflicts on every `main → variant` sync and is misleading to read. Bring it byte-identical to the canonical config on main (introduced in #88), which now fans out updates to both branches via two `updates:` entries with explicit `target-branch` fields. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-main chore(deps): mirror dependabot.yml from main (config-only)
These 5 files (file.svg, globe.svg, next.svg, vercel.svg, window.svg) were created by 'create-next-app' during initial scaffolding and have never been imported anywhere in the codebase. They were dormant assets shipped in web/public/ since project init. Removing them because Biome 2.5.0 enables the new lint/a11y/noSvgWithoutTitle rule, which fires on these files and breaks the Biome verify step in CI — blocking every grouped Dependabot non-major run that bundles the biome bump. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rom main (#117) These two workflows were added on main via #88 and #100 respectively, but the variant/document-level branch never received them. GitHub Actions reads workflow files from the PR's base branch — so Dependabot PRs and human PRs targeting variant currently bypass both the auto-merge and the Slack notification pipelines. Bringing them byte-identical from main so the variant line is treated as a true parallel main branch. After this lands: - Dependabot grouped PRs against variant get auto-merged when CI is green - All PRs against variant post to #github-logs with AI-summary Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…updates (#95) * chore(deps): bump the all-non-major group across 1 directory with 14 updates Bumps the all-non-major group with 14 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) | `2.4.16` | `2.5.0` | | [@sanity/client](https://github.com/sanity-io/client) | `7.22.0` | `7.22.1` | | [next](https://github.com/vercel/next.js) | `16.2.6` | `16.2.9` | | [next-sanity](https://github.com/sanity-io/next-sanity/tree/HEAD/packages/next-sanity) | `13.0.4` | `13.1.0` | | [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.6` | `19.2.7` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.15` | `19.2.17` | | [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.6` | `19.2.7` | | [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.3.0` | `4.3.1` | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `22.19.19` | `22.19.21` | | [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.1` | | [@sanity/code-input](https://github.com/sanity-io/plugins/tree/HEAD/plugins/@sanity/code-input) | `7.1.2` | `7.1.3` | | [@sanity/document-internationalization](https://github.com/sanity-io/plugins/tree/HEAD/plugins/@sanity/document-internationalization) | `6.2.1` | `6.2.7` | | [sanity-plugin-internationalized-array](https://github.com/sanity-io/plugins/tree/HEAD/plugins/sanity-plugin-internationalized-array) | `5.1.3` | `5.1.8` | | [sanity-plugin-media](https://github.com/sanity-io/sanity-plugin-media) | `4.3.0` | `4.3.1` | Updates `@biomejs/biome` from 2.4.16 to 2.5.0 - [Release notes](https://github.com/biomejs/biome/releases) - [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md) - [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.0/packages/@biomejs/biome) Updates `@sanity/client` from 7.22.0 to 7.22.1 - [Release notes](https://github.com/sanity-io/client/releases) - [Changelog](https://github.com/sanity-io/client/blob/main/CHANGELOG.md) - [Commits](sanity-io/client@v7.22.0...v7.22.1) Updates `next` from 16.2.6 to 16.2.9 - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](vercel/next.js@v16.2.6...v16.2.9) Updates `next-sanity` from 13.0.4 to 13.1.0 - [Release notes](https://github.com/sanity-io/next-sanity/releases) - [Changelog](https://github.com/sanity-io/next-sanity/blob/main/packages/next-sanity/CHANGELOG.md) - [Commits](https://github.com/sanity-io/next-sanity/commits/next-sanity@13.1.0/packages/next-sanity) Updates `react` from 19.2.6 to 19.2.7 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react) Updates `@types/react` from 19.2.15 to 19.2.17 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `react-dom` from 19.2.6 to 19.2.7 - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom) Updates `@tailwindcss/postcss` from 4.3.0 to 4.3.1 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-postcss) Updates `@types/node` from 22.19.19 to 22.19.21 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/react` from 19.2.15 to 19.2.17 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `tailwindcss` from 4.3.0 to 4.3.1 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss) Updates `@sanity/code-input` from 7.1.2 to 7.1.3 - [Release notes](https://github.com/sanity-io/plugins/releases) - [Changelog](https://github.com/sanity-io/plugins/blob/main/plugins/@sanity/code-input/CHANGELOG.md) - [Commits](https://github.com/sanity-io/plugins/commits/@sanity/code-input@7.1.3/plugins/@sanity/code-input) Updates `@sanity/document-internationalization` from 6.2.1 to 6.2.7 - [Release notes](https://github.com/sanity-io/plugins/releases) - [Changelog](https://github.com/sanity-io/plugins/blob/main/plugins/@sanity/document-internationalization/CHANGELOG.md) - [Commits](https://github.com/sanity-io/plugins/commits/@sanity/document-internationalization@6.2.7/plugins/@sanity/document-internationalization) Updates `sanity-plugin-internationalized-array` from 5.1.3 to 5.1.8 - [Release notes](https://github.com/sanity-io/plugins/releases) - [Changelog](https://github.com/sanity-io/plugins/blob/main/plugins/sanity-plugin-internationalized-array/CHANGELOG.md) - [Commits](https://github.com/sanity-io/plugins/commits/sanity-plugin-internationalized-array@5.1.8/plugins/sanity-plugin-internationalized-array) Updates `sanity-plugin-media` from 4.3.0 to 4.3.1 - [Release notes](https://github.com/sanity-io/sanity-plugin-media/releases) - [Changelog](https://github.com/sanity-io/sanity-plugin-media/blob/main/CHANGELOG.md) - [Commits](sanity-io/sanity-plugin-media@v4.3.0...v4.3.1) --- updated-dependencies: - dependency-name: "@biomejs/biome" dependency-version: 2.5.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: all-non-major - dependency-name: "@sanity/client" dependency-version: 7.22.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: "@sanity/code-input" dependency-version: 7.1.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: "@sanity/document-internationalization" dependency-version: 6.2.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: "@tailwindcss/postcss" dependency-version: 4.3.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: "@types/node" dependency-version: 22.19.21 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: "@types/react" dependency-version: 19.2.17 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: "@types/react" dependency-version: 19.2.17 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: next dependency-version: 16.2.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: next-sanity dependency-version: 13.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-non-major - dependency-name: react dependency-version: 19.2.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: react-dom dependency-version: 19.2.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: sanity-plugin-internationalized-array dependency-version: 5.1.8 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: sanity-plugin-media dependency-version: 4.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-non-major - dependency-name: tailwindcss dependency-version: 4.3.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: all-non-major ... Signed-off-by: dependabot[bot] <support@github.com> * chore(format): adapt breakpoints.css to biome 2.5.0's CSS formatter Biome 2.5.0 reformats multi-line @custom-variant @media wrappers into a single-line inner @media. Pre-applying the new format here so this grouped non-major Dependabot bump can pass CI on the new biome version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Damian <hi@damianrosellen.de> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [sanity](https://github.com/sanity-io/sanity/tree/HEAD/packages/sanity) from 5.27.0 to 6.0.0. - [Release notes](https://github.com/sanity-io/sanity/releases) - [Changelog](https://github.com/sanity-io/sanity/blob/main/packages/sanity/CHANGELOG.md) - [Commits](https://github.com/sanity-io/sanity/commits/v6.0.0/packages/sanity) --- updated-dependencies: - dependency-name: sanity dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [sanity-plugin-mux-input](https://github.com/sanity-io/sanity-plugin-mux-input) from 2.19.0 to 3.0.0. - [Release notes](https://github.com/sanity-io/sanity-plugin-mux-input/releases) - [Changelog](https://github.com/sanity-io/sanity-plugin-mux-input/blob/main/CHANGELOG.md) - [Commits](sanity-io/sanity-plugin-mux-input@v2.19.0...v3.0.0) --- updated-dependencies: - dependency-name: sanity-plugin-mux-input dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@sanity/vision](https://github.com/sanity-io/sanity/tree/HEAD/packages/@sanity/vision) from 5.27.0 to 6.0.0. - [Release notes](https://github.com/sanity-io/sanity/releases) - [Changelog](https://github.com/sanity-io/sanity/blob/main/packages/@sanity/vision/CHANGELOG.md) - [Commits](https://github.com/sanity-io/sanity/commits/v6.0.0/packages/@sanity/vision) --- updated-dependencies: - dependency-name: "@sanity/vision" dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@sanity/dashboard](https://github.com/sanity-io/plugins/tree/HEAD/plugins/@sanity/dashboard) from 5.0.1 to 6.0.0. - [Release notes](https://github.com/sanity-io/plugins/releases) - [Changelog](https://github.com/sanity-io/plugins/blob/main/plugins/@sanity/dashboard/CHANGELOG.md) - [Commits](https://github.com/sanity-io/plugins/commits/@sanity/dashboard@6.0.0/plugins/@sanity/dashboard) --- updated-dependencies: - dependency-name: "@sanity/dashboard" dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
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.
Important
This PR is intentionally a draft and must never be merged. It exists to document a long-lived parallel variant of the boilerplate that uses a different internationalisation strategy. Use it as a side-by-side reference; keep
mainas the canonical, field-level variant.TL;DR
This branch (
variant/document-level) is a second flavour of the boilerplate. It swaps the field-level i18n approach used onmain(sanity-plugin-internationalized-array) for document-level translation via the official Sanity plugin@sanity/document-internationalization. Pick the flavour that matches your editorial model — both are production-shaped.References #18 (now closed). The variant branch lives on as a permanent alternative — this PR is a documentation-only side-by-side and is intentionally never merged.
The two strategies in one paragraph each
Field-level (
main). Every translatable field stores all languages inside the same document as an array of{ language, value }entries. Editors see language tabs inside each field. The web app fetches the document once and runs a JavaScript resolver to pick the right entry for the current locale. Queries are locale-agnostic; the document is the unit of editing.Document-level (this variant). Every language is its own document with a
languagefield. Sibling translations are connected by atranslation.metadatadocument auto-created by the plugin. Editors see a "Translations" toolbar that switches between language variants. The web app filters at the document level (language == $locale) and consumes plain string / Portable Text fields — no runtime resolver.Neither approach is universally better. The plugin's docs lay out the trade-offs; this PR exists so you can compare both side by side in real code.
Decision guide for adopters
Use
main(field-level) when:Use this variant (document-level) when:
What changed vs
main1. Plugin swap
internationalizedArray({…})plugin block fromstudio/sanity.config.ts.documentInternationalization({ supportedLanguages, schemaTypes, languageField }).supportedLanguagesis loaded from thesiteLanguageSettingssingleton at session start (same source as onmain, identical fallback chain).sanity-plugin-internationalized-arraystays installed transitively — the document-level plugin uses it internally for itstranslation.metadata.translationsarray, but our content schemas no longer touch it.2. Schemas
All six translatable document types —
home,page,errorSettings,siteSettings,siteNav,siteCookieBanner— gain a hidden, read-onlylanguagefield. Field types convert:main)internationalizedArrayStringstringinternationalizedArrayRichTextrichTextinternationalizedArrayRichTextMediarichTextMediaThe
firstLocalizedLabelpreview helper is deleted (titles are plain strings now). The slug field onpagegains anisUniqueLocaleAgnosticvalidator so the same slug can exist for different language variants (/aboutin bothenandde).3. Studio structure & Presentation
S.document().documentId("home")now useS.documentTypeList("home")so editors see one row per language variant; the plugin's Translations toolbar handles switching inside each document.mainDocumentsregisters four routes —/,/:locale,/:slug,/:locale/:slug— with language filters on the locale-prefixed variants.locationsResolverprojectslanguagealongsideslug.currentand emits/{language}/{slug}URLs; the web proxy redirects the default-locale prefix to the canonical unprefixed URL.4. Web GROQ
homeQueryandpageBySlugQueryfilter at the document level withlanguage == $localeand project plaintitle(no array wrapper).buildRichTextMediaQuery(depth)function is replaced with a static depth-2 inline literal, eliminating the typegen blocker.internationalizedRichTextArrayField(fieldName)helper deleted;errorSettingsbody fields project plain Portable Text.languageso each language variant is emitted as a separate URL.5. Web types
web/sanity/utils/sanityLocalizedText.ts(pickLocalizedString,parseLocalizedText,pickLocalizedPortableTextBlocks,resolveLocalizedPortableTextDeepand theIntl*Entryshapes) is deleted entirely.Intl*Entry[]to plainstring/PortableTextBlock[].web/sanity/sanity.types.gen.tsnow resolves seven queries cleanly — includingHomeQueryResultandPageBySlugQueryResult, whichmaincannot currently type because of the recursive fragment. The typed-GROQ pipeline is therefore fully complete on this variant, vs. hybrid onmain.6. Fetch wrappers & routing
fetchSanityData.tsandcachedSanityQuery.tstakes a requiredlocale: stringparameter.fetchSiteLanguageSettingsstays unparameterised — it is the locale registry itself.home-{locale},page-{slug}-{locale}./api/revalidate/route.tsvalidateslanguageon the webhook payload and emits locale-aware tags.localethrough;generateStaticParamsfor[locale]/[slug]uses each page document's ownlanguageinstead of a cartesian locale × slug product.7. Components
Every
pickLocalized*call is gone —ModuleText,ModulesRenderer,LocaleNotFoundContent, andresolveSanityMetadataaccessmodule.title,data.title,errorSettings.notFoundBodydirectly. ThesiteLocaleprop drops off the module renderer chain.Bonus side-effect
Because the typegen-blocking recursive query is gone on this variant, the typed-GROQ pipeline is fully wired — all seven exported queries that the web layer fetches produce generated
*QueryResulttypes. Onmainthe pipeline is hybrid (five queries typed, the module-bearing ones hand-typed). It's the cleanest demonstration of why doc-level i18n composes more naturally with the modern Sanity tooling.Status
pnpm -r run typecheckpnpm --filter studio run generate+ diff guardpnpm --filter web run generate+ diff guardpnpm --filter studio run buildpnpm --filter web run buildpnpm lintuseOptionalChainwarning, unchanged frommain)Known follow-ups (not part of this PR)
translation.metadatadocument; the sitemap currently emits one row per language variant withoutalternates.languages. Re-adding hreflang means joiningtranslation.metadatafromsitemapPagesQueryand emitting the language map — a focused follow-up PR, not a blocker.migrateToLanguageFieldhelper for the related v5 transition; documenting a complete content migration is a separate task.How to try it locally