npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sanity-plugin-internationalized-array

v5.0.1

Published

Store localized fields in an array to save on attributes

Readme

sanity-plugin-internationalized-array

A plugin to register array fields with a custom input component to store field values in multiple languages, queryable by using a dedicated language field.

Screenshot of an internationalized input

Installation

npm install --save sanity-plugin-internationalized-array

or

yarn add sanity-plugin-internationalized-array

Usage for simple field types

Add it as a plugin in sanity.config.ts (or .js):

import {defineConfig} from 'sanity'
import {internationalizedArray} from 'sanity-plugin-internationalized-array'

export default defineConfig({
  // ...
  plugins: [
    internationalizedArray({
      languages: [
        {id: 'en', title: 'English'},
        {id: 'fr', title: 'French'},
      ],
      defaultLanguages: ['en'],
      fieldTypes: ['string'],
    }),
  ],
})

This will register two new fields to the schema, based on the settings passed into fieldTypes:

  • internationalizedArrayString an array field of:
  • internationalizedArrayStringValue an object field, with a single string field inside called value

The above config will also create an empty array item in new documents for each language in defaultLanguages. This is configured globally for all internationalized array fields.

You can pass in more registered schema-type names to generate more internationalized arrays. Use them in your schema like this:

// For example, in postType.ts

fields: [
  defineField({
    name: 'greeting',
    type: 'internationalizedArrayString',
  }),
]

Loading languages

Languages must be an array of objects with an id and title.

languages: [
  {id: 'en', title: 'English'},
  {id: 'fr', title: 'French'}
],

Or an asynchronous function that returns an array of objects with an id and title.

languages: async () => {
  const response = await fetch('https://example.com/languages')
  return response.json()
}

The async function contains a configured Sanity Client in the first parameter, allowing you to store Language options as documents. Your query should return an array of objects with an id and title.

languages: async (client) => {
  const response = await client.fetch(`*[_type == "language"]{ id, title }`)
  return response
},

Additionally, you can "pick" fields from a document, to pass into the query. For example, if you have a concept of "Markets" where only certain language fields are required in certain markets.

In this example, each language document has an array of strings called markets to declare where that language can be used. And the document being authored has a single market field.

select: {
  market: 'market'
},
languages: async (client, {market = ``}) => {
  const response = await client.fetch(
    `*[_type == "language" && $market in markets]{ id, title }`,
    {market}
  )
  return response
},

Configuring the "Add translation" buttons

The "Add translation" buttons can be positioned in one or multiple locations by configuring buttonLocations:

  • field (default) Below the internationalized array field
  • unstable__fieldAction Inside a Field Action (currently unstable)
  • document Above the document fields, these buttons will add a new language item to every internationalized array field that can be found at the root of the document. Nested internationalized arrays are not yet supported.

To control the "Add translation" button titles, configure languageDisplay. This also affects language field labels.

  • codeOnly (default) Shows only the language codes (id)
  • titleOnly Shows the language title
  • titleAndCode Shows the language title with the code in parentheses

The "Add all languages" button can be hidden with buttonAddAll.

import {defineConfig} from 'sanity'
import {internationalizedArray} from 'sanity-plugin-internationalized-array'

export default defineConfig({
  // ...
  plugins: [
    internationalizedArray({
      // ...other config
      buttonLocations: ['field', 'unstable__fieldAction', 'document'], // default ['field']
      buttonAddAll: false, // default true
      languageDisplay: 'codeOnly', // codeOnly (default) | titleOnly | titleAndCode
    }),
  ],
})

Using complex field configurations

For more control over the value field, you can pass a schema definition into the fieldTypes array.

import {defineConfig} from 'sanity'
import {internationalizedArray} from 'sanity-plugin-internationalized-array'

 export default defineConfig({
  // ...
  plugins: [
    internationalizedArray({
      languages: [
        {id: 'en', title: 'English'},
        {id: 'fr', title: 'French'}
      ],
      fieldTypes: [
        defineField({
          name: 'featuredProduct',
          type: 'reference',
          to: [{type: 'product'}]
          hidden: (({document}) => !document?.title)
        })
      ],
    })
  ]
})

This would also create two new fields in your schema.

  • internationalizedArrayFeaturedProduct an array field of:
  • internationalizedArrayFeaturedProductValue an object field, with a single string field inside called value

Note that the name key in the field gets rewritten to value and is instead used to name the object field.

Creating internationalized objects

Due to how fields are created, you cannot use anonymous objects in the fieldTypes array. You must register the object type in your Studio's schema as an "alias type".

// ./schemas/seoType.ts

import {defineField} from 'sanity'

export const seoType = defineField({
  name: 'seo',
  title: 'SEO',
  type: 'object',
  fields: [
    defineField({name: 'title', type: 'string'}),
    defineField({name: 'description', type: 'string'}),
  ],
})

Then in your plugin configuration settings, add the name of your alias type to the fieldTypes setting.

internationalizedArray({
  languages: [
    //...languages
  ],
  fieldTypes: ['seo'],
})

Lastly, add the field to your schema.

// ./schemas/post.ts

import {defineField, defineType} from 'sanity'

export default defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({
      name: 'seo',
      type: 'internationalizedArraySeo',
    }),
  ],
})

Validation of individual array items

You may wish to validate individual language fields for various reasons. From the internationalized array field, add a validation rule that can look through all the array items, and return item-specific validation messages at the path of that array item.

defineField({
  name: 'title',
  type: 'internationalizedArrayString',
  description: `Use fewer than 5 words.`,
  validation: (rule) =>
    rule.custom<{value: string; _type: string; _key: string}[]>((value) => {
      if (!value) {
        return 'Title is required'
      }

      const invalidItems = value.filter(
        (item) => item.value.split(' ').length > 5,
      )

      if (invalidItems.length) {
        return invalidItems.map((item) => ({
          message: `Title is too long. Must be 5 words or fewer.`,
          path: [{_key: item._key}, 'value'],
        }))
      }

      return true
    }),
}),

Usage with @sanity/language-filter

If you have many languages and authors that predominately write in only a few, @sanity/language-filter can be used to reduce the number of language fields shown in the document form.

Internationalized array field filtered with language-filter

The plugin includes built-in integration with @sanity/language-filter. To enable it, add languageFilter.documentTypes in the plugin config for the document types that should show the filter.

import {defineConfig} from 'sanity'
import {internationalizedArray} from 'sanity-plugin-internationalized-array'

export default defineConfig({
  // ...
  plugins: [
    internationalizedArray({
      languages: [
        {id: 'en', title: 'English'},
        {id: 'fr', title: 'French'},
      ],
      defaultLanguages: ['en'],
      fieldTypes: ['string'],
      languageFilter: {
        documentTypes: ['internationalizedPost', 'lesson'],
      },
    }),
  ],
})

If you need more control, you can continue using @sanity/language-filter directly and pass internationalizedArrayLanguageFilter from this package as your filterField.

Shape of stored data

The custom input contains buttons which will add new array items with a random _key and the language stored in a dedicated language field. Data returned from this array will look like this:

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

Querying data

Using GROQ filters you can query for a specific language like so:

*[_type == "person"] {
  "greeting": greeting[language == "en"][0].value
}

Migrate from v4 to v5

v5 stores the language identifier on a dedicated language field instead of _key.

1. Backup your data.

You can manually backup your data using the sanity CLI.

sanity dataset export production

This creates a production.tar.gz file in your current directory containing all your documents and assets.

You can also specify a custom filename and location:

sanity dataset export production ./backups/backup-2026-02-16.tar.gz

If you ever need to restore, use the import command:

sanity dataset import backup-2026-02-16.tar.gz production

Or you can use the backup service, read more at https://www.sanity.io/docs/content-lake/backups

2. Update your GROQ queries.

Use a backwards compatible query until your migration is ready and has been executed.

*[_type == "person"] {
-  "greeting": greeting[_key == "en"][0].value
+  "greeting": greeting[language == "en" || _key == "en"][0].value
}

If you use AI agents in your code, you can copy this skill to your project to guide your agents and help you with the migration.

Use this pre migration prompt

Use the project skill `i18n-array-groq-query-migration` to update this repo's GROQ queries for `sanity-plugin-internationalized-array` v5.

Goal:
- Detect all query patterns where language is read from `_key` (for example `_key == "en"` or `_key == $language`).
- Keep the same language expression used in each query.

Mode: PRE-MIGRATION (backwards-compatible)
- Replace `_key == <languageExpr>` with `language == <languageExpr> || _key == <languageExpr>`.

What to do:
1) Scan the codebase for all relevant GROQ queries.
2) Update the files in place.
3) Exclude unrelated `_key` uses that are not language matching.
4) Return a concise report with:
   - files changed
   - before/after snippets
   - any ambiguous matches needing manual review.

3. Data migration

The package exports a migration helper that you can run with the migration CLI.

Create a migration file in your project and export it:

// ./migrations/migrateToLanguageField.ts
import {migrateToLanguageField} from 'sanity-plugin-internationalized-array/migrations'

// Add the document types that contain internationalized arrays.
// Example: ['internationalizedPost', 'translation.metadata']
const DOCUMENT_TYPES: string[] = []

export default migrateToLanguageField(DOCUMENT_TYPES)

Then verify your migration with a dry run:

pnpm sanity migration run migrateToLanguageField

Once ready, run the migration:

pnpm sanity migration run migrateToLanguageField --no-dry-run

If you use @sanity/document-internationalization: Include 'translation.metadata' in your migration's document types so that the translations array on metadata documents is migrated And update @sanity/document-internationalization to v6

4. Update your GROQ queries

Previously we updated the GROQ queries to support both locations for the language field. Once migration is complete, update the GROQ queries again to only use language and remove the dependency on _key.

*[_type == "person"] {
  "greeting": greeting[language == "en"][0].value
}

If you use AI agents in your code, you can automate this with the project skill at .cursor/skills/i18n-array-groq-query-migration/SKILL.md.

Ask your AI agent to help you

# Migrate internationalized-array to v5 and document-internationalization to v6
Create and present a plan first, then wait for user confirmation before making changes.

Before running any migration steps, ask the user to create a backup and confirm it is complete. Do not proceed until backup confirmation is received.

Use this context:
- Document internationalization (v5 -> v6): https://github.com/sanity-io/plugins/blob/main/plugins/%40sanity/document-internationalization/README.md#migrating-to-v6
- Internationalized array README: https://github.com/sanity-io/plugins/blob/main/plugins/sanity-plugin-internationalized-array/README.md
- Skill source to copy from: https://github.com/sanity-io/plugins/blob/main/.claude/skills/i18n-array-groq-query-migration/SKILL.md

Then execute this workflow:
## Pre migration:
1) Copy the `i18n-array-groq-query-migration` skill file from the provided source URL into the relevant local project skill location.
2) Use the project skill `i18n-array-groq-query-migration` in PRE-MIGRATION mode and update all legacy GROQ filters from:
   `_key == <languageExpr>`
   to:
   `language == <languageExpr> || _key == <languageExpr>`
3) Keep the same `<languageExpr>` in each query, edit files in place, and report changed files plus any ambiguous cases.
4) Add a pause here, the user should deploy this changes with the queries updated before continuing with the migration, request confirmation on this step.

## Migration:
5) Update the packages to the new versions.
6) Identify schema types that need updates by scanning schemas where internationalization is used, specifically document types that contain fields with `type` matching `internationalizedArray*`.
7) Create the migration file following the internationalized array migration guidance, using the discovered document types in the previous step.
8) If `@sanity/document-internationalization` is used, include `translation.metadata` in the migration document types and plan for upgrading `@sanity/document-internationalization` to `v6`.
9) Ask the user to run the migration as dry run first, then non-dry-run only after confirmation.
10) Stop and ask the user to confirm the migration completed successfully before proceeding to post-migration query cleanup.
11) Ask the user to deploy the changes.

## Post migration.
11) After migration is confirmed complete, use the project skill in POST-MIGRATION mode.
12) Update all GROQ queries from:
   `language == <languageExpr> || _key == <languageExpr>`
   to:
   `language == <languageExpr>`
13) Keep the same `<languageExpr>` in each query, edit files in place, and report changed files plus any ambiguous cases.

Report:
- The approved plan
- Backup confirmation status
- Changed query files
- Discovered schema types for migration
- The created migration file path and summary of what it migrates

Usage with language filter

The plugin now includes built-in integration with @sanity/language-filter. To enable it, add languageFilter.documentTypes in the plugin config for the document types that should show the filter.

import {defineConfig} from 'sanity'
import {internationalizedArray} from 'sanity-plugin-internationalized-array'

export default defineConfig({
  // ...
  plugins: [
    internationalizedArray({
      languages: [
        {id: 'en', title: 'English'},
        {id: 'fr', title: 'French'},
      ],
      defaultLanguages: ['en'],
      fieldTypes: ['string'],
      languageFilter: {
        documentTypes: ['internationalizedPost', 'lesson'],
      },
    }),
  ],
})

Migrate from objects to arrays

See the migration script inside ./migrations/transformObjectToArray.ts of this plugin.

Follow the instructions inside the script and set the _type and field name you wish to target.

Please take a backup first!

Why store localized field data like this?

The most popular way to store field-level translated content is in an object using the method prescribed in @sanity/language-filter. This works well and creates tidy object structures, but also creates a unique field path for every unique field name, multiplied by the number of languages in your dataset.

For most people, this won't become an issue. On a very large dataset with a lot of languages, the Attribute Limit can become a concern. This plugin's arrays will use fewer attributes than an object once you have more than three languages.

The same content as above, plus a third language, structured as an object of string fields looks like this:

"greeting" {
  "en": "hello",
  "fr": "bonjour",
  "es": "hola"
}

Which creates four unique query paths, one for the object and one for each language.

greeting
greeting.en
greeting.fr
greeting.es

Every language you add to every object that uses this structure will add to the number of unique query paths.

The array created by this plugin creates four query paths by default, but is not affected by the number of languages:

greeting
greeting[]
greeting[]._key
greeting[].value

By using this plugin you can safely extend the number of languages without adding any additional query paths.

License

MIT © Simeon Griggs