Skip to content
This repository was archived by the owner on Feb 3, 2026. It is now read-only.

Feat/sapp 2780 language field#114

Closed
annez wants to merge 6 commits intomainfrom
feat/sapp-2780-language-field
Closed

Feat/sapp 2780 language field#114
annez wants to merge 6 commits intomainfrom
feat/sapp-2780-language-field

Conversation

@annez
Copy link
Contributor

@annez annez commented Jan 8, 2026

Summary

This PR refactors how language identification is stored in internationalized array items. Previously, the language ID was stored in the array item's _key field. Now, a dedicated language field is used, and _key contains a random identifier generated by nanoid.

Before (v3.x)

{
  "greeting": [
    { "_key": "en", "value": "hello" },
    { "_key": "fr", "value": "bonjour" }
  ]
}

After (v4.x)

{
  "greeting": [
    { "_key": "abc123", "language": "en", "value": "hello" },
    { "_key": "def456", "language": "fr", "value": "bonjour" }
  ]
}

Motivation

The _key field in Sanity arrays is meant for tracking item identity across edits, not for storing semantic data. Using it for language IDs caused issues with:

  • Array reordering and diffing in the Studio - Sanity uses _key to track which items moved/changed
  • Portable Text operations that rely on stable keys for proper diff/merge behavior
  • Edge cases when copying/pasting between documents where keys could collide

By moving the language identifier to a dedicated field, _key can fulfill its intended purpose as a stable, random identifier.

Changes

Phase 1: Core Type & Schema Changes

  • src/types.ts - Added language: string to the Value interface
  • src/schema/object.ts - Added hidden language field definition, updated preview to show language instead of _key
  • src/schema/array.ts - Updated validation logic:
    • Check for missing/empty language values
    • Check for invalid language values (not in configured languages)
    • Check for duplicate language values

Phase 2: Component Updates

  • src/components/InternationalizedInput.tsx - 7 changes from _key to language:

    • languageKeysInUse now maps v.language
    • keyIsValid checks value.language
    • handleKeyChange sets ['language'] path instead of ['_key']
    • Language lookup uses value.language
    • isDefault check uses value.language
    • UI text references value.language
  • src/components/InternationalizedArray.tsx - 4 changes:

    • handleRestoreOrder uses v.language for reordering
    • allKeysAreLanguages checks v.language
    • languagesInUse filters by v.language
    • languagesOutOfOrder compares by v.language
  • src/components/DocumentAddButtons.tsx - Updated to generate new format:

    • Added nanoid import
    • Changed from { _key: languageId } to { _key: nanoid(), language: languageId }
    • alreadyTranslated check uses translation.language

Phase 3: Utility Updates

  • src/utils/createAddLanguagePatches.ts - Core change for new item creation:

    • Added nanoid import
    • Changed from _key: id to _key: nanoid(), language: id
  • src/utils/checkAllLanguagesArePresent.ts - Changed v._key to v.language

  • src/fieldActions/index.ts - Updated disabled check from item._key to item.language

  • src/utils/getDocumentsToTranslate.ts - Added language: string to DocumentsToTranslate interface

Phase 4: Migration & Documentation

  • migrations/keyToLanguage.ts - New migration script that:

    • Finds documents with internationalized arrays lacking language field
    • Copies _key value to new language field
    • Generates new random _key using nanoid
    • Uses optimistic locking (ifRevisionID) for safe concurrent execution
    • Is idempotent - safe to run multiple times
    • Supports dry-run mode for previewing changes
    • Processes documents in configurable batches
  • README.md - Updated documentation:

    • Changed "Shape of stored data" section to show new format
    • Updated "Querying data" section with language == "en" instead of _key == "en"
    • Added new "Migrate from v3 to v4" section with:
      • Explanation of the change
      • Step-by-step migration instructions
      • Migration script usage guide
    • Updated @sanity/language-filter example for new data shape

Dependencies

  • package.json - Added nanoid: ^5.0.7 as a production dependency

GROQ Query Migration

Users will need to update their GROQ queries:

// Before (v3)
*[_type == "person"] {
  "greeting": greeting[_key == "en"][0].value
}

// After (v4)
*[_type == "person"] {
  "greeting": greeting[language == "en"][0].value
}

Migration Guide for Users

  1. Backup your data before migrating
  2. Update the plugin to v4
  3. Update GROQ queries to use language instead of _key
  4. Run the migration script:
    • Edit migrations/keyToLanguage.ts with your document types and field names
    • Run with DRY_RUN = true first to preview
    • Run with DRY_RUN = false to apply changes

Test Plan

  • Verify new items are created with { _key: nanoid(), language: id } format
  • Verify existing v3 data displays correctly after migration
  • Verify language switching works correctly
  • Verify "Add all translations" button works
  • Verify language reordering works
  • Verify validation catches:
    • Missing language values
    • Invalid language values
    • Duplicate language values
  • Verify migration script:
    • Dry-run mode previews changes correctly
    • Live mode applies changes correctly
    • Idempotent - running twice doesn't break anything
  • Verify @sanity/language-filter integration works

Breaking Changes

This is a breaking change for users upgrading from v3:

  1. Data format change - Existing documents need migration
  2. GROQ queries - Must change _key == "lang" to language == "lang"
  3. Any code that relies on _key being the language ID needs updating

A migration script and guide are included to help users upgrade.


🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 noreply@anthropic.com

annez and others added 6 commits January 8, 2026 09:59
…hema validation

Phase 1 of _key to language pivot:

- Add `language: string` field to Value type (src/types.ts)
- Add `language: string` field to InternationalizedValue type
- Update array.ts validation to check `language` instead of `_key`
- Add hidden `language` field to object schema definition
- Update object preview to display `language` as subtitle

Key design decisions:
- `_key` remains for Sanity's array item tracking (UUID)
- `language` is the new semantic identifier for language matching
- Error paths still use `_key` for Sanity to locate items

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Address review feedback from @sapphire:
- Add explicit check for missing or empty `language` field
- Clear error message: "Language is required for each array item"
- Checks before invalid language validation (fail-fast)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ition

- Add migrations/keyToLanguage.ts with full structure
- Configurable document types and field names
- Batch processing with optimistic locking (ifRevisionID)
- Dry run mode enabled by default for safety
- Idempotent GROQ query finds docs without language field
- Transform logic copies _key to language, generates new nanoid _key

The transformation logic is complete but will be tested once all phases
are merged into the feature branch.
- Update intro to reference language field instead of _key
- Update 'Shape of stored data' section with new format
- Update 'Querying data' section with language-based queries
- Add 'Migrate from v3 to v4' section with migration guide
- Update table of contents
- Update @sanity/language-filter example for new data structure
Phase 3 (Utils - from @charlie):
- createAddLanguagePatches.ts: Use nanoid() for _key, add language field
- checkAllLanguagesArePresent.ts: Check v.language instead of v._key
- fieldActions/index.ts: Update disabled check to use item.language

Phase 2 (Components - from @charlie):
- InternationalizedArray.tsx: Update 4 _key references to language
- InternationalizedInput.tsx: Update 7 _key references to language
- DocumentAddButtons.tsx: Add nanoid import, use language for checks
- getDocumentsToTranslate.ts: Add language to interface

Shared:
- package.json: Add nanoid ^5.0.7 dependency
- keyToLanguage.ts: Fix lint issues with eslint-disable

Note: Pre-existing type error in schema/array.ts:35 is unrelated to these changes.
The stricter Value type (with required `language` field) surfaced a latent
type mismatch between InternationalizedArray's ArrayOfObjectsInputProps
and defineField's expected ArrayOfPrimitivesInputProps.

The component works correctly at runtime - this is a type system limitation
where Sanity's types can't express that our array contains objects, not primitives.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@stipsan
Copy link
Member

stipsan commented Feb 3, 2026

This plugin has moved to a new plugins monorepo: https://github.com/sanity-io/plugins/tree/main/plugins/sanity-plugin-internationalized-array

@stipsan stipsan closed this Feb 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants