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

Commit 2a19fa0

Browse files
annezclaude
andauthored
feat(SAPP-2780): Use dedicated language field instead of _key for language identification (#112)
* feat(SAPP-2780): add language field to type definitions and update schema 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> * fix(SAPP-2780): add validation for missing/empty language values 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> * feat(SAPP-2780): scaffold migration script for _key to language transition - 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. * docs(SAPP-2780): update README for v4 language field changes - 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 * feat(SAPP-2780): integrate Phase 2 and Phase 3 changes 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. * fix(SAPP-2780): Add type assertion for InternationalizedArray component 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> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 714f6a7 commit 2a19fa0

14 files changed

+552
-74
lines changed

README.md

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
# sanity-plugin-internationalized-array
66

7-
A plugin to register array fields with a custom input component to store field values in multiple languages, queryable by using the language ID as an array `_key`.
7+
A plugin to register array fields with a custom input component to store field values in multiple languages, queryable by the `language` field.
88

99
![Screenshot of an internationalized input](./img/internationalized-array.png)
1010

@@ -19,6 +19,7 @@ A plugin to register array fields with a custom input component to store field v
1919
- [Usage with @sanity/language-filter](#usage-with-sanitylanguage-filter)
2020
- [Shape of stored data](#shape-of-stored-data)
2121
- [Querying data](#querying-data)
22+
- [Migrate from v3 to v4](#migrate-from-v3-to-v4)
2223
- [Migrate from objects to arrays](#migrate-from-objects-to-arrays)
2324
- [Why store localized field data like this?](#why-store-localized-field-data-like-this)
2425
- [License](#license)
@@ -308,15 +309,14 @@ export default defineConfig({
308309
enclosingType.name.startsWith('internationalizedArray') &&
309310
'kind' in member
310311
) {
311-
// Get last two segments of the field's path
312-
const pathEnd = member.field.path.slice(-2)
313-
// If the second-last segment is a _key, and the last segment is `value`,
314-
// It's an internationalized array value
315-
// And the array _key is the language of the field
316-
const language =
317-
pathEnd[1] === 'value' && isKeySegment(pathEnd[0])
318-
? pathEnd[0]._key
319-
: null
312+
// Get the language from the member's parent value
313+
// In v4+, language is stored in a dedicated `language` field
314+
const parentValue = member.field.path.length >= 2
315+
? member.field.document?.[member.field.path[0]]?.find(
316+
(item: any) => item._key === member.field.path[1]?._key
317+
)
318+
: null
319+
const language = parentValue?.language
320320

321321
return language ? selectedLanguageIds.includes(language) : false
322322
}
@@ -339,25 +339,93 @@ export default defineConfig({
339339

340340
## Shape of stored data
341341

342-
The custom input contains buttons which will add new array items with the language as the `_key` value. Data returned from this array will look like this:
342+
The custom input contains buttons which will add new array items with a `language` field identifying the language. Data returned from this array will look like this:
343343

344344
```json
345345
"greeting": [
346-
{ "_key": "en", "value": "hello" },
347-
{ "_key": "fr", "value": "bonjour" },
346+
{ "_key": "abc123", "language": "en", "value": "hello" },
347+
{ "_key": "def456", "language": "fr", "value": "bonjour" }
348348
]
349349
```
350350

351+
> **Note:** In versions prior to v4, the language ID was stored in the `_key` field. See [Migrate from v3 to v4](#migrate-from-v3-to-v4) if you're upgrading from an earlier version.
352+
351353
## Querying data
352354

353-
Using GROQ filters you can query for a specific language key like so:
355+
Using GROQ filters you can query for a specific language like so:
354356

355357
```js
356358
*[_type == "person"] {
357-
"greeting": greeting[_key == "en"][0].value
359+
"greeting": greeting[language == "en"][0].value
358360
}
359361
```
360362

363+
> **Migrating queries from v3:** If upgrading from v3, replace `_key == "en"` with `language == "en"` in your GROQ queries.
364+
365+
## Migrate from v3 to v4
366+
367+
Version 4 changes how language identification is stored. 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.
368+
369+
**Before (v3):**
370+
```json
371+
{ "_key": "en", "value": "hello" }
372+
```
373+
374+
**After (v4):**
375+
```json
376+
{ "_key": "abc123", "language": "en", "value": "hello" }
377+
```
378+
379+
### Why this change?
380+
381+
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:
382+
- Array reordering and diffing in the Studio
383+
- Portable Text operations that rely on stable keys
384+
- Edge cases when copying/pasting between documents
385+
386+
### Migration steps
387+
388+
1. **Take a backup first!**
389+
```bash
390+
npx sanity@latest dataset export
391+
```
392+
393+
2. **Update the plugin** to v4
394+
395+
3. **Update your GROQ queries** to use `language` instead of `_key`:
396+
```js
397+
// Before
398+
greeting[_key == "en"][0].value
399+
400+
// After
401+
greeting[language == "en"][0].value
402+
```
403+
404+
4. **Run the migration script** to update existing documents:
405+
406+
Edit `migrations/keyToLanguage.ts` to configure your document types and field names:
407+
```ts
408+
const DOCUMENT_TYPES = ['post', 'page'] // Your document types
409+
const FIELD_NAMES = ['title', 'description'] // Your internationalized fields
410+
```
411+
412+
First, run in dry-run mode to preview changes:
413+
```bash
414+
npx sanity@latest exec ./migrations/keyToLanguage.ts --with-user-token
415+
```
416+
417+
Then set `DRY_RUN = false` and run again to apply changes.
418+
419+
5. **Handle drafts and published documents** - The migration script processes all documents. Run it twice if needed: once for production, once after publishing any pending drafts.
420+
421+
### Migration script details
422+
423+
The migration script (`migrations/keyToLanguage.ts`):
424+
- Processes documents in batches of 100
425+
- Uses optimistic locking (`ifRevisionID`) for safe concurrent execution
426+
- Is idempotent - safe to run multiple times
427+
- Skips items that already have a `language` field
428+
361429
## Migrate from objects to arrays
362430

363431
[See the migration script](https://github.com/sanity-io/sanity-plugin-internationalized-array/blob/main/migrations/transformObjectToArray.ts) inside `./migrations/transformObjectToArray.ts` of this Repo.

0 commit comments

Comments
 (0)