diff --git a/.changeset/studio-smartling-monorepo-migration.md b/.changeset/studio-smartling-monorepo-migration.md
new file mode 100644
index 000000000..494cd164e
--- /dev/null
+++ b/.changeset/studio-smartling-monorepo-migration.md
@@ -0,0 +1,15 @@
+---
+'sanity-plugin-studio-smartling': major
+---
+
+Port sanity-plugin-studio-smartling to the Sanity plugins monorepo
+
+This major release includes several breaking changes as part of the migration to the monorepo:
+
+- **React Compiler enabled**: The plugin is now built with React Compiler targeting React 19
+- **ESM-only**: CommonJS support has been removed. The package now ships only ESM
+- **React 19.2+ required**: Minimum React version is now 19.2 (previously ^18.3 || ^19)
+- **react-dom 19.2+ required**: `react-dom` is now a required peer dependency
+- **Sanity Studio v5+ required**: Minimum Sanity version is now v5 (Sanity v3 and v4 are no longer supported)
+- **Node.js 20.19+ required**: Minimum Node.js version is now 20.19 (previously >=14)
+- **styled-components 6.1+ required**: `styled-components` is now a required peer dependency (required by `sanity-translations-tab`)
diff --git a/README.md b/README.md
index 878e82416..9243fd269 100644
--- a/README.md
+++ b/README.md
@@ -101,6 +101,7 @@ Sessions can be compared in the DevTools UI to diff bundle changes between build
| [`sanity-plugin-internationalized-array`](./plugins/sanity-plugin-internationalized-array) | Store localized fields in arrays to save attributes |
| [`sanity-plugin-latex-input`](./plugins/sanity-plugin-latex-input) | LaTeX input for Portable Text Editor |
| [`sanity-plugin-markdown`](./plugins/sanity-plugin-markdown) | Markdown editor input |
+| [`sanity-plugin-studio-smartling`](./plugins/sanity-plugin-studio-smartling) | In-studio integration with Smartling for content translation |
| [`sanity-plugin-transifex`](./plugins/sanity-plugin-transifex) | In-studio integration with Transifex for content translation |
| [`sanity-naive-html-serializer`](./plugins/sanity-naive-html-serializer) | Serialize Sanity documents and rich text fields to HTML |
| [`sanity-plugin-utils`](./plugins/sanity-plugin-utils) | Handy hooks and components for Sanity Studio plugins |
diff --git a/dev/test-studio/package.json b/dev/test-studio/package.json
index 430fd6e67..3430fda48 100644
--- a/dev/test-studio/package.json
+++ b/dev/test-studio/package.json
@@ -43,6 +43,7 @@
"sanity-plugin-internationalized-array": "workspace:*",
"sanity-plugin-latex-input": "workspace:*",
"sanity-plugin-markdown": "workspace:*",
+ "sanity-plugin-studio-smartling": "workspace:*",
"sanity-plugin-transifex": "workspace:*",
"sanity-plugin-utils": "workspace:*",
"sanity-plugin-workflow": "workspace:*",
diff --git a/dev/test-studio/sanity.config.ts b/dev/test-studio/sanity.config.ts
index 06d536c3d..aa52db8e6 100644
--- a/dev/test-studio/sanity.config.ts
+++ b/dev/test-studio/sanity.config.ts
@@ -33,6 +33,7 @@ import {richDateInputExample} from '#rich-date-input'
import {sanityNaiveHtmlSerializerExample} from '#sanity-naive-html-serializer'
import {scriptRunnerTool} from '#script-runner'
import {sfccExample} from '#sfcc'
+import {smartlingExample} from '#smartling'
import {studioSecretsExample} from '#studio-secrets'
import {transifexExample} from '#transifex'
import {translationsTabExample} from '#translations-tab'
@@ -65,6 +66,7 @@ export default defineConfig([
createWorkspace({name: 'utils-example', title: 'Utils Example', plugins: [utilsExample()]}),
createWorkspace({name: 'iframe-pane-example', plugins: [iframePaneExample()]}),
createWorkspace({name: 'transifex-example', title: 'Transifex', plugins: [transifexExample()]}),
+ createWorkspace({name: 'smartling-example', title: 'Smartling', plugins: [smartlingExample()]}),
createWorkspace({name: 'documents-pane-example', plugins: [documentsPaneExample()]}),
createWorkspace({
name: 'translations-tab-example',
diff --git a/dev/test-studio/src/smartling/index.tsx b/dev/test-studio/src/smartling/index.tsx
new file mode 100644
index 000000000..262cf85d8
--- /dev/null
+++ b/dev/test-studio/src/smartling/index.tsx
@@ -0,0 +1,72 @@
+import {EarthGlobeIcon} from '@sanity/icons'
+import {defineField, definePlugin, defineType} from 'sanity'
+import {defaultFieldLevelConfig, TranslationsTab} from 'sanity-plugin-studio-smartling'
+import type {DefaultDocumentNodeResolver} from 'sanity/structure'
+import {structureTool} from 'sanity/structure'
+
+const languages = [
+ {id: 'en', title: 'English', isDefault: true},
+ {id: 'es', title: 'Spanish'},
+ {id: 'fr', title: 'French'},
+]
+
+const localizedString = defineType({
+ name: 'smartlingLocalizedString',
+ type: 'object',
+ fieldsets: [
+ {
+ title: 'Translations',
+ name: 'translations',
+ options: {collapsible: true, collapsed: false},
+ },
+ ],
+ fields: languages.map((lang) =>
+ defineField({
+ name: lang.id,
+ title: lang.title,
+ type: 'string',
+ fieldset: lang.isDefault ? undefined : 'translations',
+ }),
+ ),
+})
+
+const smartlingTest = defineType({
+ type: 'document',
+ name: 'smartlingTest',
+ title: 'Smartling',
+ icon: EarthGlobeIcon,
+ fields: [
+ defineField({type: 'string', name: 'title', title: 'Title'}),
+ defineField({
+ type: 'smartlingLocalizedString',
+ name: 'greeting',
+ title: 'Greeting',
+ }),
+ defineField({
+ type: 'smartlingLocalizedString',
+ name: 'description',
+ title: 'Description',
+ }),
+ ],
+})
+
+const defaultDocumentNode: DefaultDocumentNodeResolver = (S, {schemaType}) => {
+ if (schemaType === 'smartlingTest') {
+ return S.document().views([
+ S.view.form(),
+ S.view.component(TranslationsTab).title('Smartling').options(defaultFieldLevelConfig),
+ ])
+ }
+
+ return S.document().views([S.view.form()])
+}
+
+export const smartlingExample = definePlugin(() => ({
+ name: 'smartling-example',
+ schema: {types: [localizedString, smartlingTest]},
+ plugins: [
+ structureTool({
+ defaultDocumentNode,
+ }),
+ ],
+}))
diff --git a/knip.jsonc b/knip.jsonc
index adffbb2a4..8f207be53 100644
--- a/knip.jsonc
+++ b/knip.jsonc
@@ -174,6 +174,11 @@
],
},
// add new plugin workspaces here
+ "plugins/sanity-plugin-studio-smartling": {
+ "entry": ["package.config.ts"],
+ "project": ["src/**/*.{ts,tsx}"],
+ },
+
"plugins/sanity-plugin-documents-pane": {
"entry": ["package.config.ts"],
"project": ["src/**/*.{ts,tsx}"],
diff --git a/plugins/sanity-plugin-studio-smartling/CHANGELOG.md b/plugins/sanity-plugin-studio-smartling/CHANGELOG.md
new file mode 100644
index 000000000..cd9a7eab3
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/CHANGELOG.md
@@ -0,0 +1,19 @@
+# sanity-plugin-studio-smartling
+
+## [4.3.3](https://github.com/sanity-io/sanity-plugin-studio-smartling/compare/v4.3.2...v4.3.3) (2026-01-07)
+
+### Bug Fixes
+
+- **deps:** Update dependency sanity-translations-tab to v5 ([#35](https://github.com/sanity-io/sanity-plugin-studio-smartling/issues/35)) ([a64c6d8](https://github.com/sanity-io/sanity-plugin-studio-smartling/commit/a64c6d883ccfecacb5ea5adc335b17008bd5db75))
+
+## [4.3.2](https://github.com/sanity-io/sanity-plugin-studio-smartling/compare/v4.3.1...v4.3.2) (2025-12-29)
+
+### Bug Fixes
+
+- update package.json and package-lock.json to support Sanity v5 ([#34](https://github.com/sanity-io/sanity-plugin-studio-smartling/issues/34)) ([9a8eb09](https://github.com/sanity-io/sanity-plugin-studio-smartling/commit/9a8eb0978b27c4b7c848835645e5c8d99d3d07c3))
+
+## [4.3.1](https://github.com/sanity-io/sanity-plugin-studio-smartling/compare/v4.3.0...v4.3.1) (2025-07-10)
+
+### Bug Fixes
+
+- **deps:** allow studio v4 peer dep ranges ([1e3de60](https://github.com/sanity-io/sanity-plugin-studio-smartling/commit/1e3de60b44d0867bedfb5b91b01c0ee8d6047798))
diff --git a/plugins/sanity-plugin-studio-smartling/README.md b/plugins/sanity-plugin-studio-smartling/README.md
new file mode 100644
index 000000000..cff345f20
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/README.md
@@ -0,0 +1,206 @@
+## Installation
+
+```sh
+npm install sanity-plugin-studio-smartling
+```
+
+# Studio Plugin for Sanity & Smartling
+
+
+
+We're proud to be partnered with Smartling and their [official connector](https://help.smartling.com/hc/en-us/articles/1260803085050-Sanity-Connector-Overview-) makes it quick and easy to get your studio content into your Smartling project.
+
+This is a separate plugin, and differs in that it provides editors a visual progress bar for ongoing translations and a way to import translations back into your content at either the document or field level. Feel free to try it out and see which solution works for you!
+
+_Recent updates for v4:_ We've added support for the new document internationalization plugin pattern. Please read the [Document level translations](#document-level-translations) section for more information.
+
+# Table of Contents
+
+- [Quickstart](#quickstart)
+- [Assumptions](#assumptions)
+- [Studio experience](#studio-experience)
+- [Overriding defaults](#overriding-defaults)
+- [License](#license)
+- [Develop and test](#develop-and-test)
+
+## Quickstart
+
+1. In your studio folder, run:
+
+```sh
+npm install sanity-plugin-studio-smartling
+```
+
+2. Because of Smartling CORS restrictions, you will need to set up a proxy endpoint to funnel requests to Smartling. We've provided a tiny Next.js app you can set up [here](https://github.com/sanity-io/example-sanity-smartling-proxy). If that's not useful, the important thing to pay attention to is that this endpoint handles requests with an `X-URL` header that contains the Smartling URL configured by the plugin, and can parse a data file to an HTML string and send it back to the adapter.
+
+3. Create or use a Smartling project token.
+
+[Please refer to the Smartling documentation on creating a token if you don't have one already.](https://help.smartling.com/hc/en-us/articles/115004187694-API-Tokens)
+
+In your Studio folder, create a file called `populateSmartlingSecrets.js` with the following contents:
+
+```javascript
+// ./populateSmartlingSecrets.js
+// Do not commit this file to your repository
+
+import {getCliClient} from 'sanity/cli'
+
+const client = getCliClient({apiVersion: '2023-02-15'})
+
+client.createOrReplace({
+ // The `.` in this _id will ensure the document is private
+ // even in a public dataset!
+ _id: 'translationService.secrets',
+ _type: 'smartlingSettings',
+ //replace these with your values
+ organization: 'YOUR_SMARTLING_ORGANIZATION_HERE',
+ project: 'YOUR_SMARTLING_PROJECT_HERE',
+ secret: '{"userIdentifier":"xxxxxx","userSecret":"xxxx"}', //in this format from Smartling when you press the button "copy token" on creation
+ proxy: 'my-proxy-endpoint.com/api/proxy', //the endpoint you set up in step 2
+})
+```
+
+On the command line, run the file:
+
+```sh
+npx sanity exec populateSmartlingSecrets.js --with-user-token
+```
+
+Verify that the document was created using the Vision Tool in the Studio and query `*[_id == 'translationService.secrets']`. Note: If you have multiple datasets, you'll have to do this across all of them.
+
+If the document was found in your dataset(s), delete `populateSmartlingSecrets.js`.
+
+If you have concerns about this being exposed to authenticated users of your studio, you can control access to this path with [role-based access control](https://www.sanity.io/docs/access-control).
+
+4. Get the Smartling tab on your desired document type, using whatever pattern you like. You'll use the [desk structure](https://www.sanity.io/docs/structure-builder-introduction) for this. The options for translation will be nested under this desired document type's views. Here's an example:
+
+```javascript
+import {DefaultDocumentNodeResolver} from 'sanity/desk'
+//...your other desk structure imports...
+import {TranslationsTab, defaultDocumentLevelConfig} from 'sanity-plugin-studio-smartling'
+//if you are using field-level translations, you can import the field-level config instead:
+//import {TranslationsTab, defaultFieldLevelConfig} from 'sanity-plugin-studio-smartling'
+//if you're not sure which, please look at the document-level and field-level sections below
+
+export const defaultDocumentNode: DefaultDocumentNodeResolver = (S, {schemaType}) => {
+ if (schemaType === 'myTranslatableDocumentType') {
+ return S.document().views([
+ S.view.form(),
+ //...my other views -- for example, live preview, document pane, etc.,
+ S.view.component(TranslationsTab).title('Smartling').options(defaultDocumentLevelConfig)
+ //again, if you're using field-level translations, you can use the field-level config instead:
+ //S.view.component(TranslationsTab).title('Smartling').options(defaultFieldLevelConfig)
+ ])
+ }
+ return S.document()
+}
+```
+
+And that should do it! Go into your studio, click around, and check the document in Smartling (it should be under its Sanity `_id` by default, but you can override this). Once it's translated, check the import by clicking the `Import` button on your Smartling tab!
+
+## Assumptions
+
+To use the default config mentioned above, we assume that you are following the conventions we outline in [our documentation on localization](https://www.sanity.io/docs/localization).
+
+### Field-level translations
+
+If you are using field-level translation and the `defaultFieldLevelConfig` configuration, we assume any fields you want translated exist in the multi-locale object form we recommend.
+For example, a non-localizable "title" field will be a flat string: `title: 'My title is here.'` For a field you want to include many languages for, your title may look like
+`
+{
+ title: {
+ en: 'My title is here.',
+ es_ES: 'Mi título está aquí.',
+ etc...
+ }
+}
+ `
+_Important_: Smartling's locale representation includes hyphens, like `fr-FR`. These aren't valid as Sanity field names, so ensure that on your fields you change the hyphens to underscores (like `fr_FR`).
+
+### Document level translations
+
+Since we often find users want to use the [Document internationalization plugin](https://www.sanity.io/plugins/document-internationalization) if they're using document-level translations, we assume that any documents you want in different languages will be present in a `translation.metadata` document.
+
+_Important_: The above is true if you are using the Document Internationalization Plugin at version 2 or above. If you are using version 1 please use the `legacyDocumentLevelConfig` configuration exported from this plugin. This configuration assumes your translations follow the pattern `{id-of-base-language-document}__i18n_{locale}`
+
+### Final note
+
+It's okay if your data doesn't follow these patterns and you don't want to change them! You will simply have to override how the plugin gets and patches back information from your documents. Please see [Overriding defaults](#overriding-defaults).
+
+## Studio experience
+
+By adding the `TranslationsTab` to your desk structure, your users should now have an additional view on their document. The boxes at the top of the tab can be used to send translations off to Smartling, and once those jobs are started, they should see progress bars monitoring the progress of the jobs. They can import a partial or complete job back. They can also re-send a document, which should update the existing job.
+
+## Overriding defaults
+
+To personalize this configuration it's useful to know what arguments go into `TranslationsTab` as options (the `defaultConfigs` are just wrappers for these):
+
+- `exportForTranslation`: a function that takes your document id and returns an object like:
+
+```javascript
+{
+ `name`: /*the field you want to use identify your doc in Smartling (by default this is `_id`) */
+ `content`: /* a serialized HTML string of all the fields in your document to be translated. */
+}
+```
+
+- `importTranslation`: a function that takes in `id` (your document id), `localeId` (the locale of the imported language), and `document` (the translated HTML from Smartling). It will deserialize your document back into an object that can be patched into your Sanity data, and then executes that patch.
+- `Adapter`: An interface with methods to send things over to Smartling. You likely don't want to override this!
+
+There are several reasons to override these functions. Generally, developers will customize to ensure documents serialize and deserialize correctly. Since the serialization functions are used across all our translation plugins currently, you can find some frequently encountered scenarios at [their repository here](https://github.com/sanity-io/sanity-naive-html-serializer), along with code examples for customized configurations.
+
+## Migrating to Sanity Studio v3
+
+There is one major breaking change in this plugin's migration to Sanity Studio v3: the proxy was set in an environment variable, and now it should be part of the `secrets` document.
+
+In v2, you would set the proxy in a `.env` file, like so:
+
+```env
+SANITY_STUDIO_SMARTLING_PROXY=https://my-proxy-endpoint.com/api/proxy
+```
+
+In v3, you should set the proxy in the `secrets` document. If you have an existing secrets document, you can patch it like so:
+
+```javascript
+// ./patchSmartlingSecrets.js
+// Do not commit this file to your repository
+
+import {getCliClient} from 'sanity/cli'
+
+const client = getCliClient({apiVersion: '2023-02-15'})
+
+client.patch('translationService.secrets').set({proxy: 'https://my-proxy.com/api/proxy'}).commit()
+```
+
+and run the script with `sanity exec patchSmartlingSecrets.js --with-user-token`.
+
+Alternatively, you can re-run the `populateSmartlingSecrets` script in [Quickstart](#quickstart) to create a new secrets document with the proxy set.
+
+We apologize for the inconvenience. Because of the new embeddability of the studio, developers may find that their v3 Studio is built and deployed in different ways, with access to different environments. Keeping this setting in `secrets` allows developers to set it in a way that works for their deployment and reduce complexity. You can find more information on our guidance around environment variables [here](https://github.com/sanity-io/sanity/releases/tag/v3.5.0).
+
+Otherwise, you should not have to do anything to migrate to Sanity Studio v3. If you are using the default configs, you should be able to upgrade without any changes. If you are using custom serialization, you may need to update how `BaseDocumentSerializer` receives your schema.
+
+These are outlined in the serializer README [here](https://github.com/sanity-io/sanity-naive-html-serializer#v2-to-v3-changes).
+
+The final change from the v2 to v3 version of the plugin is in how progress in a translation job is calculated. The plugin will now count progress as the percentage of all strings that have reached the final stage of a Smartling workflow.
+
+## License
+
+[MIT](LICENSE) © Sanity.io
+
+## Develop & test
+
+This plugin is in early stages. We plan on improving some of the user-facing chrome, sorting out some quiet bugs, figuring out where things don't fail elegantly, etc. Please be a part of our development process!
+
+This plugin uses [@sanity/plugin-kit](https://github.com/sanity-io/plugin-kit)
+with default configuration for build & watch scripts.
+
+See [Testing a plugin in Sanity Studio](https://github.com/sanity-io/plugin-kit#testing-a-plugin-in-sanity-studio)
+on how to run this plugin with hotreload in the studio.
+
+### Release new version
+
+Run ["CI & Release" workflow](https://github.com/sanity-io/sanity-plugin-transifex/actions/workflows/main.yml).
+Make sure to select the main branch and check "Release new version".
+
+Semantic release will only release on configured branches, so it is safe to run release on any branch.
diff --git a/plugins/sanity-plugin-studio-smartling/docs/00-faq.md b/plugins/sanity-plugin-studio-smartling/docs/00-faq.md
new file mode 100644
index 000000000..cb4ea6325
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/docs/00-faq.md
@@ -0,0 +1,60 @@
+## Where should I place the Translations Tab?
+
+The Translations Tab should be invoked as a view. This can be done in the `getDefaultDocumentNode` function in `schemas.ts`:
+
+```ts
+import {
+ TranslationsTab,
+ defaultDocumentLevelConfig,
+ defaultFieldLevelConfig,
+} from 'sanity-plugin-studio-smartling'
+
+export const defaultDocumentNodeResolver = (S) =>
+ S.document().views([
+ // Give all documents the JSON preview,
+ // as well as the default form view
+ S.view.form(),
+ S.view.component(TranslationsTab).options(defaultDocumentLevelConfig),
+ ])
+```
+
+If using a document-level translation pattern, you should likely only include this view in your "base" language. Please see the document internationalization plugin on building different desk structure options for different locales. (example code coming soon)
+
+## What happens when I click the "Create Job" button?
+
+The tab will do the following:
+
+1. Fetch the latest draft of the document you're currently in.
+2. Filter out (as best as it can) any fields that should not be translated (references, dates, numbers, etc.). It will also utilize options passed in to ignore certain fields and objects. See more in the Advanced Configuration docs.
+3. Serialize the document into an HTML string. It will utilize options to serialize objects in particular ways, if provided.
+4. Send the HTML string to the translation vendor's API, along with the locale code of the language(s) you want to translate to.
+5. Look up the translation job ID in the response from the translation vendor (this will either match your document ID or be invoked by a custom job name resolver [coming soon]).
+6. Show you the progress of the ongoing translation and a link to the job in the vendor, if available.
+
+## How am I seeing my progress?
+
+On load, the tab fetches the total amount of strings that have reached the LAST stage of translation and are ready to be imported. That is divided over the total amount of strings in the document, and the progress bar is updated accordingly.
+
+## How do I import translations?
+
+When the translation vendor has completed the translation, you can click the "Import Translations" button. This will do the following:
+
+1. Deserialize the HTML string from the translation vendor into a Sanity document.
+2. Fetch the revision of the document you're currently in at the time the translation was sent, if available. (This is to resolve the translation as smoothly as possible, in case the document has changed since it was sent to translation and cannot resolve conflicts)
+3. Compare the two documents and merge the translated content with anything that was not sent for translation.
+4. Issue a patch with the relevant merges to the relevant document. If using document internationalization, this will also update translation metadata.
+
+## How can I prevent certain fields from being sent to translation?
+
+You can pass in a `stopTypes` parameter to name all objects you do not want translated. Alternately, the serializer also introspects your schema. You can set `localize: false` on any field you do not want translated.
+
+```js
+ fields: [
+ defineField({
+ name: 'categories',
+ type: 'array',
+ //ts-ignore
+ localize: false,
+ ...
+ })]
+```
diff --git a/plugins/sanity-plugin-studio-smartling/docs/01-advanced-configuration.md b/plugins/sanity-plugin-studio-smartling/docs/01-advanced-configuration.md
new file mode 100644
index 000000000..a9a100df5
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/docs/01-advanced-configuration.md
@@ -0,0 +1,76 @@
+## Advanced configuration
+
+This plugin provides defaults for most configuration options, but they can be overridden as needed. Please refer to the types to see what you should declare, but we also provide the type for all options, which we recommend using for quicker development.
+
+```typescript
+// sanity.config.ts
+import {
+ TranslationsTab,
+ defaultDocumentLevelConfig,
+ defaultFieldLevelConfig,
+ type TranslationsTabConfigOptions,
+} from 'sanity-plugin-studio-smartling'
+
+const customConfig = {
+ ...defaultDocumentLevelConfig,
+ // baseLanguage is `en` by default, overwrite as needed. This is important for coalescing translations and creating accurate translation metadata.
+ baseLanguage: 'en_US',
+ // this is the field that will be used to determine the language of the document. It is `language` by default.
+ languageField: 'locale',
+ serializationOptions: {
+ // use this option to exclude objects that do not need to be translated. The plugin already uses defaults for references, dates, numbers, etc.
+ additionalStopTypes: ['colorTheme'],
+ // use this option to serialize objects in a particular way. The plugin will try to filter and serialize objects as best as it can, but you can override this behavior here.
+ // For now, you should also include these for annotations and inline objects in PortableText, if you want them to be translated.
+ // NOTE: it is VERY important to include the _type as a class and the _key as id for accurately deserializing and coalescing
+ additionalSerializers: {
+ testObject: ({value}) => {
+ return `${value.title}`
+ },
+ },
+ // Create a method to deserialize any custom serialization rules
+ additonalDeserializers: {
+ testObject: ({node}) => {
+ return {
+ _type: 'testObject',
+ _key: node.id,
+ title: node.textContent,
+ }
+ },
+ },
+ // Block text requires a special deserialization format based on @sanity/block-tools. Use this option for any annotations or inline objects that need to be translated.
+ additionalBlockDeserializers: [
+ {
+ deserialize(el, next): TypedObject | undefined {
+ if (!el.className || el.className?.toLowerCase() !== 'myannotation') {
+ return undefined
+ }
+
+ const markDef = {
+ _key: el.id,
+ _type: 'myAnnotation',
+ }
+
+ return {
+ _type: '__annotation',
+ markDef: markDef,
+ children: next(el.childNodes),
+ }
+ },
+ },
+ ],
+ },
+ // sometimes editors will duplicate a field or document and begin changing assets
+ // THEN import the translation. this option merges your translated strings with
+ // the already-existing translated document or field.
+ mergeWithTargetLocale: true,
+ // adapter, baseLanguage, secretsNamespace, importTranslation, exportForTranslation should likely not be touched unless you very much want to customize your plugin.
+} satisfies TranslationsTabConfigOptions
+
+const defaultDocumentNode: DefaultDocumentNodeResolver = (S) => {
+ return S.document().views([
+ S.view.form(),
+ S.view.component(TranslationsTab).title('Smartling').options(customConfig),
+ ])
+}
+```
diff --git a/plugins/sanity-plugin-studio-smartling/package.config.ts b/plugins/sanity-plugin-studio-smartling/package.config.ts
new file mode 100644
index 000000000..43da34cfa
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/package.config.ts
@@ -0,0 +1,8 @@
+import config from '@repo/package.config'
+import {defineConfig} from '@sanity/pkg-utils'
+
+export default defineConfig({
+ ...config,
+ babel: {reactCompiler: true},
+ reactCompilerOptions: {target: '19'},
+})
diff --git a/plugins/sanity-plugin-studio-smartling/package.json b/plugins/sanity-plugin-studio-smartling/package.json
new file mode 100644
index 000000000..062142abe
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "sanity-plugin-studio-smartling",
+ "version": "4.3.3",
+ "description": "This plugin provides an in-studio integration with Smartling. It allows your editors to send any document to Smartling with the click of a button, monitor ongoing translations, and import partial or complete translations back into the studio.",
+ "keywords": [
+ "sanity",
+ "sanity-plugin"
+ ],
+ "homepage": "https://github.com/sanity-io/plugins/tree/main/plugins/sanity-plugin-studio-smartling#readme",
+ "bugs": {
+ "url": "https://github.com/sanity-io/plugins/issues"
+ },
+ "license": "MIT",
+ "author": "Sanity.io ",
+ "repository": {
+ "type": "git",
+ "url": "git+ssh://git@github.com/sanity-io/plugins.git",
+ "directory": "plugins/sanity-plugin-studio-smartling"
+ },
+ "files": [
+ "dist"
+ ],
+ "type": "module",
+ "types": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "source": "./src/index.ts",
+ "development": "./src/index.ts",
+ "default": "./dist/index.js"
+ },
+ "./package.json": "./package.json"
+ },
+ "publishConfig": {
+ "exports": {
+ ".": "./dist/index.js",
+ "./package.json": "./package.json"
+ }
+ },
+ "scripts": {
+ "build": "pkg build --strict --check --clean",
+ "prepack": "turbo run build"
+ },
+ "dependencies": {
+ "sanity-translations-tab": "workspace:*"
+ },
+ "devDependencies": {
+ "@repo/package.config": "workspace:*",
+ "@repo/tsconfig": "workspace:*",
+ "@sanity/pkg-utils": "catalog:",
+ "@types/react": "catalog:",
+ "@types/react-dom": "catalog:",
+ "babel-plugin-react-compiler": "catalog:",
+ "react": "catalog:",
+ "react-dom": "catalog:",
+ "sanity": "catalog:",
+ "styled-components": "catalog:"
+ },
+ "peerDependencies": {
+ "react": "^19.2",
+ "react-dom": "^19.2",
+ "sanity": "^5 || ^6.0.0-0",
+ "styled-components": "^6.1"
+ },
+ "engines": {
+ "node": ">=20.19 <22 || >=22.12"
+ }
+}
diff --git a/plugins/sanity-plugin-studio-smartling/src/adapter/createTask.ts b/plugins/sanity-plugin-studio-smartling/src/adapter/createTask.ts
new file mode 100644
index 000000000..9b1ba8bb6
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/src/adapter/createTask.ts
@@ -0,0 +1,160 @@
+import type {Adapter, Secrets, SerializedDocument} from 'sanity-translations-tab'
+
+import {getTranslationTask} from './getTranslationTask'
+import {authenticate, getHeaders, findExistingJob} from './helpers'
+
+const createJob = (
+ jobName: string,
+ secrets: Secrets,
+ localeIds: string[],
+ accessToken: string,
+ documentId: string,
+) => {
+ const {project, proxy} = secrets
+ if (!project || !proxy) {
+ throw new Error(
+ 'The Smartling adapter requires a Smartling project identifier and a proxy URL. Please check your secrets document in this dataset, per the plugin documentation.',
+ )
+ }
+
+ const url = `https://api.smartling.com/jobs-api/v3/projects/${project}/jobs`
+ return fetch(proxy, {
+ method: 'POST',
+ headers: {
+ ...getHeaders(url, accessToken),
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify({
+ jobName,
+ targetLocaleIds: localeIds,
+ referenceNumber: documentId,
+ }),
+ })
+ .then((res) => res.json())
+ .then((res) => res.response.data.translationJobUid)
+}
+
+/* we're using batches here because it eliminates some
+ * new string authorization issues for updating existing jobs,
+ * and is able to be used for new bulk
+ * job functionality.
+ */
+
+const createJobBatch = (
+ jobId: string,
+ secrets: Secrets,
+ documentId: string,
+ accessToken: string,
+ localeIds: string[],
+ workflowUid?: string,
+) => {
+ const {project, proxy} = secrets
+ if (!project || !proxy) {
+ throw new Error(
+ 'The Smartling adapter requires a Smartling project identifier and a proxy URL. Please check your secrets document in this dataset, per the plugin documentation.',
+ )
+ }
+ const url = `https://api.smartling.com/job-batches-api/v2/projects/${project}/batches`
+ const reqBody: {
+ authorize: boolean
+ translationJobUid: string
+ fileUris: string[]
+ localeWorkflows?: {targetLocaleId: string; workflowUid: string}[]
+ } = {
+ authorize: true,
+ translationJobUid: jobId,
+ fileUris: [documentId],
+ }
+
+ if (workflowUid) {
+ reqBody.localeWorkflows = localeIds.map((l) => ({
+ targetLocaleId: l,
+ workflowUid,
+ }))
+ }
+
+ return fetch(proxy, {
+ method: 'POST',
+ headers: {
+ ...getHeaders(url, accessToken),
+ 'content-type': 'application/json',
+ },
+ body: JSON.stringify(reqBody),
+ })
+ .then((res) => res.json())
+ .then((res) => res.response.data.batchUid)
+}
+
+const uploadFileToBatch = (
+ batchUid: string,
+ documentId: string,
+ document: SerializedDocument,
+ secrets: Secrets,
+ localeIds: string[],
+ accessToken: string,
+ callbackUrl?: string,
+) => {
+ const {project, proxy} = secrets
+ if (!project || !proxy) {
+ throw new Error(
+ 'The Smartling adapter requires a Smartling project identifier and a proxy URL. Please check your secrets document in this dataset, per the plugin documentation.',
+ )
+ }
+ const url = `https://api.smartling.com/job-batches-api/v2/projects/${project}/batches/${batchUid}/file`
+ const formData = new FormData()
+ formData.append('fileUri', documentId)
+ formData.append('fileType', 'html')
+ formData.append('file', new Blob([document.content]), `${document.name}.html`)
+ localeIds.forEach((localeId) => formData.append('localeIdsToAuthorize[]', localeId))
+ if (callbackUrl) {
+ formData.append('callbackUrl', callbackUrl)
+ }
+
+ return fetch(proxy, {
+ method: 'POST',
+ headers: getHeaders(url, accessToken),
+ body: formData,
+ }).then((res) => res.json())
+}
+
+export const createTask: Adapter['createTask'] = async (
+ documentId: string,
+ document: SerializedDocument,
+ localeIds: string[],
+ secrets: Secrets | null,
+ workflowUid?: string,
+ callbackUrl?: string,
+) => {
+ if (!secrets?.project || !secrets?.secret || !secrets?.proxy) {
+ throw new Error(
+ 'The Smartling adapter requires a project ID, a secret key, and a proxy URL. Please check your secrets document in this dataset, per the plugin documentation.',
+ )
+ }
+
+ const accessToken = await authenticate(secrets)
+
+ let taskId = await findExistingJob(document.name, secrets, accessToken)
+ if (!taskId) {
+ taskId = await createJob(document.name, secrets, localeIds, accessToken, documentId)
+ }
+
+ const batchUid = await createJobBatch(
+ taskId,
+ secrets,
+ documentId,
+ accessToken,
+ localeIds,
+ workflowUid,
+ )
+ await uploadFileToBatch(
+ batchUid,
+ documentId,
+ document,
+ secrets,
+ localeIds,
+ accessToken,
+ callbackUrl,
+ )
+
+ return getTranslationTask(documentId, secrets)
+}
diff --git a/plugins/sanity-plugin-studio-smartling/src/adapter/getLocales.ts b/plugins/sanity-plugin-studio-smartling/src/adapter/getLocales.ts
new file mode 100644
index 000000000..aa8e5560f
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/src/adapter/getLocales.ts
@@ -0,0 +1,18 @@
+import type {Adapter, Secrets} from 'sanity-translations-tab'
+
+import {authenticate, getHeaders} from './helpers'
+
+export const getLocales: Adapter['getLocales'] = async (secrets: Secrets | null) => {
+ if (!secrets?.project || !secrets?.secret || !secrets?.proxy) {
+ return []
+ }
+ const {project, proxy} = secrets
+ const url = `https://api.smartling.com/projects-api/v2/projects/${project}`
+ const accessToken = await authenticate(secrets)
+ return fetch(proxy, {
+ method: 'GET',
+ headers: getHeaders(url, accessToken),
+ })
+ .then((res) => res.json())
+ .then((res) => res.response.data.targetLocales)
+}
diff --git a/plugins/sanity-plugin-studio-smartling/src/adapter/getTranslation.ts b/plugins/sanity-plugin-studio-smartling/src/adapter/getTranslation.ts
new file mode 100644
index 000000000..2ad13b004
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/src/adapter/getTranslation.ts
@@ -0,0 +1,37 @@
+import type {Adapter, Secrets} from 'sanity-translations-tab'
+
+import {authenticate, getHeaders} from './helpers'
+
+export const getTranslation: Adapter['getTranslation'] = async (
+ taskId: string,
+ localeId: string,
+ secrets: Secrets | null,
+) => {
+ if (!secrets?.project || !secrets?.secret || !secrets?.proxy) {
+ throw new Error(
+ 'The Smartling adapter requires a project ID, a secret key, and a proxy URL. Please check your secrets document in this dataset, per the plugin documentation.',
+ )
+ }
+
+ const {project, proxy} = secrets
+
+ const url = `https://api.smartling.com/files-api/v2/projects/${project}/locales/${localeId}/file?fileUri=${taskId}&retrievalType=pending`
+ const accessToken = await authenticate(secrets)
+ const translatedHTML = await fetch(proxy, {
+ method: 'GET',
+ headers: getHeaders(url, accessToken),
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ if (res.body) {
+ return res.body
+ } else if (res.response.errors) {
+ const errMsg =
+ res.response.errors[0]?.message || 'Error retrieving translation from Smartling'
+ throw new Error(errMsg)
+ }
+ return ''
+ })
+
+ return translatedHTML
+}
diff --git a/plugins/sanity-plugin-studio-smartling/src/adapter/getTranslationTask.ts b/plugins/sanity-plugin-studio-smartling/src/adapter/getTranslationTask.ts
new file mode 100644
index 000000000..4138bfd4b
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/src/adapter/getTranslationTask.ts
@@ -0,0 +1,86 @@
+import type {Adapter, Secrets} from 'sanity-translations-tab'
+
+import {authenticate, getHeaders, findExistingJob} from './helpers'
+
+interface WorkflowProgressItem {
+ workflowStepSummaryReportItemList: {
+ wordCount: number
+ }[]
+}
+
+interface SmartlingProgressItem {
+ targetLocaleId: string
+ progress: {
+ percentComplete: number
+ totalWordCount: number
+ }
+ workflowProgressReportList: WorkflowProgressItem[]
+}
+
+export const getTranslationTask: Adapter['getTranslationTask'] = async (
+ documentId: string,
+ secrets: Secrets | null,
+) => {
+ if (!secrets?.project || !secrets?.secret || !secrets?.proxy) {
+ return {
+ documentId,
+ taskId: documentId,
+ locales: [],
+ }
+ }
+
+ const {project, proxy} = secrets
+
+ const accessToken = await authenticate(secrets)
+ const taskId = await findExistingJob(documentId, secrets, accessToken)
+ if (!taskId) {
+ return {
+ documentId,
+ taskId: documentId,
+ locales: [],
+ }
+ }
+
+ const progressUrl = `https://api.smartling.com/jobs-api/v3/projects/${project}/jobs/${taskId}/progress`
+ const smartlingTask = await fetch(proxy, {
+ method: 'GET',
+ headers: getHeaders(progressUrl, accessToken),
+ })
+ .then((res) => res.json())
+ .then((res) => res.response.data)
+
+ let locales = []
+ if (smartlingTask && smartlingTask.contentProgressReport) {
+ locales = smartlingTask.contentProgressReport.map((item: SmartlingProgressItem) => {
+ let progress = item.progress ? item.progress.percentComplete : 0
+ //default to the first workflow -- it's likely what is being used
+ const progressItem = item.workflowProgressReportList?.[0]
+ if (progressItem && item.progress) {
+ //this is a list of the various steps in the workflow
+ if (
+ progressItem.workflowStepSummaryReportItemList &&
+ progressItem.workflowStepSummaryReportItemList.length > 1
+ ) {
+ //get the last step in the workflow -- usually "published"
+ const lastStep = progressItem.workflowStepSummaryReportItemList.at(-1)
+ //get the percentage of how many words have reached the last step
+ if (lastStep && lastStep.wordCount >= 0) {
+ progress = Math.floor((lastStep.wordCount / item.progress.totalWordCount) * 100) ?? 0
+ }
+ }
+ }
+ return {
+ localeId: item.targetLocaleId,
+ progress,
+ }
+ })
+ }
+
+ return {
+ documentId,
+ locales,
+ //since our download is tied to document id for smartling, keep track of it as a task
+ taskId: documentId,
+ linkToVendorTask: `https://dashboard.smartling.com/app/projects/${project}/account-jobs/${project}:${taskId}`,
+ }
+}
diff --git a/plugins/sanity-plugin-studio-smartling/src/adapter/helpers.ts b/plugins/sanity-plugin-studio-smartling/src/adapter/helpers.ts
new file mode 100644
index 000000000..591b27ddd
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/src/adapter/helpers.ts
@@ -0,0 +1,80 @@
+import type {Secrets} from 'sanity-translations-tab'
+
+export const authenticate = (secrets: Secrets): Promise => {
+ const url = 'https://api.smartling.com/auth-api/v2/authenticate'
+ const headers = {
+ 'content-type': 'application/json',
+ 'X-URL': url,
+ }
+ const {secret, proxy} = secrets
+ if (!secret || !proxy) {
+ throw new Error(
+ 'The Smartling adapter requires a secret key and a proxy URL. Please check your secrets document in this dataset, per the plugin documentation.',
+ )
+ }
+ return fetch(proxy, {
+ headers,
+ method: 'POST',
+ body: JSON.stringify(secret),
+ })
+ .then((res) => res.json())
+ .then((res) => res.response.data.accessToken)
+}
+
+export const getHeaders = (url: string, accessToken: string): Record => ({
+ 'Authorization': `Bearer ${accessToken}`,
+ 'X-URL': url,
+})
+
+export const findExistingJob = async (
+ documentId: string,
+ secrets: Secrets,
+ accessToken: string,
+): Promise => {
+ const {project, proxy} = secrets
+ if (!project || !proxy) {
+ throw new Error(
+ 'The Smartling adapter requires a Smartling project identifier and a proxy URL. Please check your secrets document in this dataset, per the plugin documentation.',
+ )
+ }
+ const url = `https://api.smartling.com/jobs-api/v3/projects/${project}/jobs?jobName=${documentId}`
+ //first, try fetching from name resolution
+ let items = await fetch(proxy, {
+ headers: getHeaders(url, accessToken),
+ })
+ .then((res) => res.json())
+ .then((res) => res?.response?.data?.items)
+
+ if (!items || !items.length) {
+ //if that fails, try fetching by fileUri and check the referenceNumber
+ const refUrl = `https://api.smartling.com/jobs-api/v3/projects/${project}/jobs/search`
+ items = await fetch(proxy, {
+ headers: {
+ ...getHeaders(refUrl, accessToken),
+ 'content-type': 'application/json',
+ },
+ method: 'POST',
+ body: JSON.stringify({
+ fileUris: [documentId],
+ }),
+ })
+ .then((res) => res.json())
+ .then((res) => res?.response?.data?.items)
+ }
+
+ if (items.length) {
+ //smartling will fuzzy match job names. We need to be precise.
+ const correctJob = items
+ .filter((item: {jobStatus: string}) => item.jobStatus !== 'DELETED')
+ .find(
+ (item: {jobName: string; referenceNumber: string}) =>
+ (item.jobName && item.jobName === documentId) ||
+ (item.referenceNumber && item.referenceNumber === documentId),
+ )
+
+ if (correctJob) {
+ return correctJob.translationJobUid
+ }
+ }
+ return ''
+}
diff --git a/plugins/sanity-plugin-studio-smartling/src/adapter/index.ts b/plugins/sanity-plugin-studio-smartling/src/adapter/index.ts
new file mode 100644
index 000000000..d952abb6a
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/src/adapter/index.ts
@@ -0,0 +1,13 @@
+import type {Adapter} from 'sanity-translations-tab'
+
+import {createTask} from './createTask'
+import {getLocales} from './getLocales'
+import {getTranslation} from './getTranslation'
+import {getTranslationTask} from './getTranslationTask'
+
+export const SmartlingAdapter: Adapter = {
+ getLocales,
+ getTranslationTask,
+ createTask,
+ getTranslation,
+}
diff --git a/plugins/sanity-plugin-studio-smartling/src/index.test.ts b/plugins/sanity-plugin-studio-smartling/src/index.test.ts
new file mode 100644
index 000000000..58bc95e3e
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/src/index.test.ts
@@ -0,0 +1,32 @@
+import {fileURLToPath} from 'node:url'
+
+import {expect, test} from 'vitest'
+import {getPackageExportsManifest} from 'vitest-package-exports'
+
+test('package exports', {timeout: 30_000}, async () => {
+ const manifest = await getPackageExportsManifest({
+ importMode: 'dist',
+ cwd: fileURLToPath(import.meta.url),
+ })
+
+ expect(manifest.exports).toMatchInlineSnapshot(`
+ {
+ ".": {
+ "BaseDocumentDeserializer": "object",
+ "BaseDocumentMerger": "object",
+ "BaseDocumentSerializer": "function",
+ "SmartlingAdapter": "object",
+ "TranslationsTab": "function",
+ "customSerializers": "object",
+ "defaultDocumentLevelConfig": "object",
+ "defaultFieldLevelConfig": "object",
+ "defaultStopTypes": "object",
+ "documentLevelPatch": "function",
+ "fieldLevelPatch": "function",
+ "findLatestDraft": "function",
+ "legacyDocumentLevelConfig": "object",
+ "legacyDocumentLevelPatch": "function",
+ },
+ }
+ `)
+})
diff --git a/plugins/sanity-plugin-studio-smartling/src/index.ts b/plugins/sanity-plugin-studio-smartling/src/index.ts
new file mode 100644
index 000000000..106733af5
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/src/index.ts
@@ -0,0 +1,47 @@
+import {
+ baseDocumentLevelConfig,
+ baseFieldLevelConfig,
+ legacyDocumentLevelConfig as baseLegacyDocumentLevelConfig,
+} from 'sanity-translations-tab'
+import type {TranslationsTabConfigOptions} from 'sanity-translations-tab'
+
+import {SmartlingAdapter} from './adapter'
+
+export {
+ BaseDocumentDeserializer,
+ BaseDocumentMerger,
+ BaseDocumentSerializer,
+ customSerializers,
+ defaultStopTypes,
+ documentLevelPatch,
+ fieldLevelPatch,
+ findLatestDraft,
+ legacyDocumentLevelPatch,
+ TranslationsTab,
+} from 'sanity-translations-tab'
+export type {
+ TranslationFunctionContext,
+ TranslationsTabConfigOptions,
+} from 'sanity-translations-tab'
+
+const defaultDocumentLevelConfig: TranslationsTabConfigOptions = {
+ ...baseDocumentLevelConfig,
+ adapter: SmartlingAdapter,
+}
+
+const legacyDocumentLevelConfig: TranslationsTabConfigOptions = {
+ ...baseLegacyDocumentLevelConfig,
+ adapter: SmartlingAdapter,
+}
+
+const defaultFieldLevelConfig: TranslationsTabConfigOptions = {
+ ...baseFieldLevelConfig,
+ adapter: SmartlingAdapter,
+}
+
+export {
+ defaultDocumentLevelConfig,
+ defaultFieldLevelConfig,
+ legacyDocumentLevelConfig,
+ SmartlingAdapter,
+}
diff --git a/plugins/sanity-plugin-studio-smartling/tsconfig.build.json b/plugins/sanity-plugin-studio-smartling/tsconfig.build.json
new file mode 100644
index 000000000..a3da93d3e
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/tsconfig.build.json
@@ -0,0 +1,8 @@
+{
+ "extends": "@repo/tsconfig/build.json",
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
+ "exclude": ["dist", "node_modules"],
+ "compilerOptions": {
+ "isolatedDeclarations": false
+ }
+}
diff --git a/plugins/sanity-plugin-studio-smartling/tsconfig.json b/plugins/sanity-plugin-studio-smartling/tsconfig.json
new file mode 100644
index 000000000..82b6db379
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "@repo/tsconfig/check.json",
+ "include": ["**/*.ts", "**/*.tsx"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/plugins/sanity-plugin-studio-smartling/vitest.config.ts b/plugins/sanity-plugin-studio-smartling/vitest.config.ts
new file mode 100644
index 000000000..8eda73152
--- /dev/null
+++ b/plugins/sanity-plugin-studio-smartling/vitest.config.ts
@@ -0,0 +1,11 @@
+import {defineConfig} from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ server: {
+ deps: {
+ inline: ['vitest-package-exports'],
+ },
+ },
+ },
+})
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b12d7ff5d..80fddf664 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -215,6 +215,9 @@ importers:
sanity-plugin-markdown:
specifier: workspace:*
version: link:../../plugins/sanity-plugin-markdown
+ sanity-plugin-studio-smartling:
+ specifier: workspace:*
+ version: link:../../plugins/sanity-plugin-studio-smartling
sanity-plugin-transifex:
specifier: workspace:*
version: link:../../plugins/sanity-plugin-transifex
@@ -855,7 +858,7 @@ importers:
version: 3.2.0(@emotion/is-prop-valid@1.4.0)(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)
sanity-plugin-internationalized-array:
specifier: ^4.0.0 || ^5.0.0
- version: 5.1.0(441437c418c9e9d7911ac9236d8633fd)
+ version: 5.1.0(44a9f5073b6df31c1236e2e47b6ff065)
devDependencies:
'@repo/package.config':
specifier: workspace:*
@@ -1397,7 +1400,7 @@ importers:
dependencies:
'@sanity/assist':
specifier: ^6.0.2
- version: 6.1.0(@emotion/is-prop-valid@1.4.0)(@sanity/mutator@6.0.0(@types/react@19.2.14))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))
+ version: 6.1.1(@emotion/is-prop-valid@1.4.0)(@sanity/mutator@6.0.0(@types/react@19.2.14))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))
'@sanity/icons':
specifier: ^3.7.4
version: 3.7.4(react@19.2.7)
@@ -1540,6 +1543,43 @@ importers:
specifier: 'catalog:'
version: 6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3)
+ plugins/sanity-plugin-studio-smartling:
+ dependencies:
+ sanity-translations-tab:
+ specifier: workspace:*
+ version: link:../sanity-translations-tab
+ styled-components:
+ specifier: npm:@sanity/styled-components@latest
+ version: '@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7)'
+ devDependencies:
+ '@repo/package.config':
+ specifier: workspace:*
+ version: link:../../packages/@repo/package.config
+ '@repo/tsconfig':
+ specifier: workspace:*
+ version: link:../../packages/@repo/tsconfig
+ '@sanity/pkg-utils':
+ specifier: 'catalog:'
+ version: 10.5.4(@types/babel__core@7.20.5)(@types/node@25.9.2)(@typescript/native-preview@7.0.0-dev.20260418.1)(babel-plugin-react-compiler@1.0.0)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(typescript@5.9.3)
+ '@types/react':
+ specifier: 'catalog:'
+ version: 19.2.14
+ '@types/react-dom':
+ specifier: 'catalog:'
+ version: 19.2.3(@types/react@19.2.14)
+ babel-plugin-react-compiler:
+ specifier: 'catalog:'
+ version: 1.0.0
+ react:
+ specifier: 'catalog:'
+ version: 19.2.7
+ react-dom:
+ specifier: 'catalog:'
+ version: 19.2.7(react@19.2.7)
+ sanity:
+ specifier: 'catalog:'
+ version: 6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@25.9.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@25.9.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3)
+
plugins/sanity-plugin-transifex:
dependencies:
sanity-translations-tab:
@@ -4857,8 +4897,8 @@ packages:
resolution: {integrity: sha512-dlEmALjQ5iyQG0O8ZVmkkE3wUYCKfRmiyMvuuGN5SF9buAHxmseBOKJ/Iy2DU/8ef70mtUXlzeCRSlTN/nmZsg==}
engines: {node: '>=18'}
- '@sanity/assist@6.1.0':
- resolution: {integrity: sha512-1GxyRwCiCH53doeaEGX2VR7Dc19hS3iTKel+GgEftDYY4Oof1VIi5/K+ZpcFKDgRGK2bsnyi2weVOf+IHKQP1A==}
+ '@sanity/assist@6.1.1':
+ resolution: {integrity: sha512-jSpkEsSD3e7d/NxxmKDto68PhrlmdIYlrIM7nv5QuQ1daGbUWdsSJ1X8pUM6rNmL1PrkJczSfCpj2cxoIxmnKA==}
engines: {node: '>=20.19 <22 || >=22.12'}
peerDependencies:
'@sanity/mutator': ^5 || ^6.0.0-0
@@ -4998,8 +5038,8 @@ packages:
resolution: {integrity: sha512-skhIX8gT/hLritEBkjfc7+TBlJNu/NLisyA8noKceCk28OatFK0wX7dIuFawkt3pfhTYVomVPykAYFcIm2OqJg==}
engines: {node: '>=18.2'}
- '@sanity/language-filter@5.0.3':
- resolution: {integrity: sha512-QR1Orff5ZG84XcKjKSAJh2JxECNFNySx90GKUZQHDGch8bJLTpAgmqXY51c5a0b416T+z3rGWpi6bZnBkCSwyg==}
+ '@sanity/language-filter@5.0.4':
+ resolution: {integrity: sha512-RcgKPl8k+61CILtTEtG2T5LKMc85A4zN5ktg9ISZK45izthr3RuykZpyFW+Fdk2BJqxulDImyBeH8d7rOLZsyA==}
engines: {node: '>=20.19 <22 || >=22.12'}
peerDependencies:
react: ^19.2
@@ -13293,7 +13333,7 @@ snapshots:
'@sanity/asset-utils@2.3.0': {}
- '@sanity/assist@6.1.0(@emotion/is-prop-valid@1.4.0)(@sanity/mutator@6.0.0(@types/react@19.2.14))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))':
+ '@sanity/assist@6.1.1(@emotion/is-prop-valid@1.4.0)(@sanity/mutator@6.0.0(@types/react@19.2.14))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))':
dependencies:
'@portabletext/types': 4.0.2
'@sanity/client': 7.22.1
@@ -14077,7 +14117,7 @@ snapshots:
'@sanity/json-match@1.0.5': {}
- '@sanity/language-filter@5.0.3(@emotion/is-prop-valid@1.4.0)(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))':
+ '@sanity/language-filter@5.0.4(@emotion/is-prop-valid@1.4.0)(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))':
dependencies:
'@sanity/icons': 3.7.4(react@19.2.7)
'@sanity/ui': 3.2.0(@emotion/is-prop-valid@1.4.0)(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)
@@ -18774,10 +18814,10 @@ snapshots:
safer-buffer@2.1.2: {}
- sanity-plugin-internationalized-array@5.1.0(441437c418c9e9d7911ac9236d8633fd):
+ sanity-plugin-internationalized-array@5.1.0(44a9f5073b6df31c1236e2e47b6ff065):
dependencies:
'@sanity/icons': 3.7.4(react@19.2.7)
- '@sanity/language-filter': 5.0.3(@emotion/is-prop-valid@1.4.0)(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))
+ '@sanity/language-filter': 5.0.4(@emotion/is-prop-valid@1.4.0)(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))
'@sanity/ui': 3.2.0(@emotion/is-prop-valid@1.4.0)(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)
'@sanity/util': 5.21.0(@types/react@19.2.14)
lodash-es: 4.18.1
@@ -18785,7 +18825,7 @@ snapshots:
react-dom: 19.2.7(react@19.2.7)
sanity: 6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3)
optionalDependencies:
- '@sanity/assist': 6.1.0(@emotion/is-prop-valid@1.4.0)(@sanity/mutator@6.0.0(@types/react@19.2.14))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))
+ '@sanity/assist': 6.1.1(@emotion/is-prop-valid@1.4.0)(@sanity/mutator@6.0.0(@types/react@19.2.14))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react-is@19.2.7)(react@19.2.7)(sanity@6.0.0(@babel/core@7.29.7)(@babel/runtime@7.29.7)(@emotion/is-prop-valid@1.4.0)(@noble/hashes@2.2.0)(@oclif/core@4.11.4)(@sanity/cli-core@2.0.1(@noble/hashes@2.2.0)(@types/node@24.13.2)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(terser@5.46.1)(yaml@2.9.0))(@sanity/styled-components@6.1.24(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/node@24.13.2)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(babel-plugin-react-compiler@1.0.0)(esbuild@0.28.0)(jiti@2.7.0)(oxfmt@0.41.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(rolldown@1.1.0)(terser@5.46.1)(typescript@5.9.3))
transitivePeerDependencies:
- '@emotion/is-prop-valid'
- '@types/react'