From 3a5a91159a83204169959f7dffdaddf10fef1310 Mon Sep 17 00:00:00 2001 From: Snorre Eskeland Brekke Date: Wed, 10 May 2023 12:01:17 +0200 Subject: [PATCH 001/163] feat(i18n): proof of concept i18n framework --- dev/test-studio/components/Branding.tsx | 4 +- dev/test-studio/locales/en-US/schema.ts | 26 +++ dev/test-studio/locales/en-US/testStudio.ts | 13 ++ dev/test-studio/locales/no-NB/schema.ts | 27 +++ dev/test-studio/locales/no-NB/testStudio.ts | 13 ++ dev/test-studio/locales/types.ts | 16 ++ .../async-translation/I18nLazyChild.tsx | 7 + .../async-translation/I18nLazyParent.tsx | 38 ++++ .../plugins/async-translation/plugin.tsx | 15 ++ dev/test-studio/sanity.config.ts | 19 +- .../schema/debug/i18nSchemaTranslation.ts | 50 +++++ dev/test-studio/schema/index.ts | 4 + .../portableText/allTheBellsAndWhistles.ts | 2 +- dev/test-studio/structure/constants.ts | 1 + dev/test-studio/structure/resolveStructure.ts | 5 +- packages/sanity/package.json | 2 + .../src/core/config/configPropertyReducers.ts | 52 ++++- .../sanity/src/core/config/prepareConfig.ts | 9 + packages/sanity/src/core/config/types.ts | 157 ++++++++++++- .../core/config/useConfigContextFromSource.ts | 16 +- packages/sanity/src/core/hooks/useTimeAgo.ts | 56 +++-- .../src/core/i18n/components/I18nProvider.tsx | 64 ++++++ .../core/i18n/components/LanguageContext.tsx | 67 ++++++ .../i18n/components/TranslationLoader.tsx | 63 ++++++ .../sanity/src/core/i18n/defineHelpers.ts | 8 + packages/sanity/src/core/i18n/i18nConfig.ts | 119 ++++++++++ packages/sanity/src/core/i18n/i18nHooks.ts | 78 +++++++ .../sanity/src/core/i18n/i18nNamespaces.ts | 8 + packages/sanity/src/core/i18n/i18nSchema.ts | 206 ++++++++++++++++++ packages/sanity/src/core/i18n/index.ts | 8 + .../sanity/src/core/i18n/languageStore.ts | 24 ++ .../src/core/i18n/locales/en-US/studio.ts | 71 ++++++ .../src/core/i18n/locales/no-NB/studio.ts | 72 ++++++ .../src/core/i18n/locales/types/index.ts | 2 + .../src/core/i18n/locales/types/schema.ts | 6 + .../src/core/i18n/locales/types/studio.ts | 6 + .../src/core/i18n/localizedLanguages.tsx | 27 +++ .../src/core/i18n/studioLocaleLoader.ts | 16 ++ packages/sanity/src/core/index.ts | 1 + .../sanity/src/core/studio/StudioProvider.tsx | 5 +- .../components/PlaceholderSearchInput.tsx | 5 +- .../navbar/userMenu/LanguageMenu.tsx | 62 ++++++ .../components/navbar/userMenu/UserMenu.tsx | 4 +- .../components/navbar/userMenu/index.ts | 1 + packages/sanity/src/core/studio/source.tsx | 1 + packages/sanity/src/desk/deskTool.ts | 20 +- .../desk/documentActions/PublishAction.tsx | 54 ++--- .../sanity/src/desk/i18n/deskLocaleLoader.ts | 15 ++ .../sanity/src/desk/i18n/i18nNamespaces.ts | 4 + packages/sanity/src/desk/i18n/index.ts | 2 + .../src/desk/i18n/locales/en-US/desk.ts | 48 ++++ .../src/desk/i18n/locales/no-NB/desk.ts | 49 +++++ .../src/desk/i18n/locales/types/desk.ts | 6 + packages/sanity/src/desk/index.ts | 1 + packages/sanity/src/desk/types.ts | 4 + yarn.lock | 39 ++++ 56 files changed, 1628 insertions(+), 70 deletions(-) create mode 100644 dev/test-studio/locales/en-US/schema.ts create mode 100644 dev/test-studio/locales/en-US/testStudio.ts create mode 100644 dev/test-studio/locales/no-NB/schema.ts create mode 100644 dev/test-studio/locales/no-NB/testStudio.ts create mode 100644 dev/test-studio/locales/types.ts create mode 100644 dev/test-studio/plugins/async-translation/I18nLazyChild.tsx create mode 100644 dev/test-studio/plugins/async-translation/I18nLazyParent.tsx create mode 100644 dev/test-studio/plugins/async-translation/plugin.tsx create mode 100644 dev/test-studio/schema/debug/i18nSchemaTranslation.ts create mode 100644 packages/sanity/src/core/i18n/components/I18nProvider.tsx create mode 100644 packages/sanity/src/core/i18n/components/LanguageContext.tsx create mode 100644 packages/sanity/src/core/i18n/components/TranslationLoader.tsx create mode 100644 packages/sanity/src/core/i18n/defineHelpers.ts create mode 100644 packages/sanity/src/core/i18n/i18nConfig.ts create mode 100644 packages/sanity/src/core/i18n/i18nHooks.ts create mode 100644 packages/sanity/src/core/i18n/i18nNamespaces.ts create mode 100644 packages/sanity/src/core/i18n/i18nSchema.ts create mode 100644 packages/sanity/src/core/i18n/index.ts create mode 100644 packages/sanity/src/core/i18n/languageStore.ts create mode 100644 packages/sanity/src/core/i18n/locales/en-US/studio.ts create mode 100644 packages/sanity/src/core/i18n/locales/no-NB/studio.ts create mode 100644 packages/sanity/src/core/i18n/locales/types/index.ts create mode 100644 packages/sanity/src/core/i18n/locales/types/schema.ts create mode 100644 packages/sanity/src/core/i18n/locales/types/studio.ts create mode 100644 packages/sanity/src/core/i18n/localizedLanguages.tsx create mode 100644 packages/sanity/src/core/i18n/studioLocaleLoader.ts create mode 100644 packages/sanity/src/core/studio/components/navbar/userMenu/LanguageMenu.tsx create mode 100644 packages/sanity/src/desk/i18n/deskLocaleLoader.ts create mode 100644 packages/sanity/src/desk/i18n/i18nNamespaces.ts create mode 100644 packages/sanity/src/desk/i18n/index.ts create mode 100644 packages/sanity/src/desk/i18n/locales/en-US/desk.ts create mode 100644 packages/sanity/src/desk/i18n/locales/no-NB/desk.ts create mode 100644 packages/sanity/src/desk/i18n/locales/types/desk.ts diff --git a/dev/test-studio/components/Branding.tsx b/dev/test-studio/components/Branding.tsx index 8ab3d34da16..5180a5ba5e5 100644 --- a/dev/test-studio/components/Branding.tsx +++ b/dev/test-studio/components/Branding.tsx @@ -1,10 +1,12 @@ import {Box, Text} from '@sanity/ui' import React from 'react' +import {useTranslation} from 'sanity' export function Branding() { + const {t} = useTranslation('testStudio') return ( - Test Studio™ + {t('studio.logo.title')}™ ) } diff --git a/dev/test-studio/locales/en-US/schema.ts b/dev/test-studio/locales/en-US/schema.ts new file mode 100644 index 00000000000..8d47f22ecc4 --- /dev/null +++ b/dev/test-studio/locales/en-US/schema.ts @@ -0,0 +1,26 @@ +import {defineLanguageBundle, schemaI18nNamespace} from 'sanity' + +export const schemaI18nNamespaceStrings = { + 'i18nDocument.type-title': 'i18n: English title', + 'i18nDocument.title.field-title': 'Title (en)', + 'i18nDocument.title.field-description': 'Title Description (en)', + 'i18nDocument.array.field-title': 'Array (en)', + 'i18nDocument.array.field-description': 'Array description (en)', + 'i18nDocument.ref.field-title': 'Ref (en)', + 'i18nDocument.ref.field-description': 'Ref description (en)', + + 'i18nDocument.fieldset.fieldset-title': 'Fieldset (en)', + 'i18nDocument.fieldset.fieldset-description': 'Fieldset description (en)', + + 'i18nDocument.group.group-title': 'Group (en)', + 'i18nDocument.group.group-description': 'Group description (en)', + + 'i18nArray.book.item-title': 'Book (en)', + + 'i18nRef.book.ref-title': 'Book (en)', +} + +export default defineLanguageBundle({ + namespace: schemaI18nNamespace, + resources: schemaI18nNamespaceStrings, +}) diff --git a/dev/test-studio/locales/en-US/testStudio.ts b/dev/test-studio/locales/en-US/testStudio.ts new file mode 100644 index 00000000000..a5ba51f53be --- /dev/null +++ b/dev/test-studio/locales/en-US/testStudio.ts @@ -0,0 +1,13 @@ +import {defineLanguageBundle} from 'sanity' + +export const testStudioI18nNamespace = 'testStudio' as const + +export const testStudioI18nNamespaceStrings = { + 'studio.logo.title': 'English logo', + 'structure.root.title': 'Content', +} + +export default defineLanguageBundle({ + namespace: testStudioI18nNamespace, + resources: testStudioI18nNamespaceStrings, +}) diff --git a/dev/test-studio/locales/no-NB/schema.ts b/dev/test-studio/locales/no-NB/schema.ts new file mode 100644 index 00000000000..39dc140801d --- /dev/null +++ b/dev/test-studio/locales/no-NB/schema.ts @@ -0,0 +1,27 @@ +/* eslint-disable camelcase */ +import {defineLanguageBundle, schemaI18nNamespace} from 'sanity' +import {TestStudioSchemaTranslations} from '../types' + +export const schemaI18nNamespaceStrings: Partial = { + 'i18nDocument.type-title': 'i18n: Norsk tittel', + 'i18nDocument.title.field-title': 'Tittel', + 'i18nDocument.title.field-description': 'Tittelbeskrivelse', + 'i18nDocument.array.field-title': 'Liste', + 'i18nDocument.array.field-description': 'Listebeskrivelse', + 'i18nDocument.ref.field-title': 'Referanse', + 'i18nDocument.ref.field-description': 'Referansebeskrivelse', + + 'i18nDocument.fieldset.fieldset-title': 'Feltsett', + 'i18nDocument.fieldset.fieldset-description': 'Beskrivelse for feltsett', + + 'i18nDocument.group.group-title': 'Gruppe', + + 'i18nArray.book.item-title': 'Book (en)', + + 'i18nRef.book.ref-title': 'Book (en)', +} + +export default defineLanguageBundle({ + namespace: schemaI18nNamespace, + resources: schemaI18nNamespaceStrings, +}) diff --git a/dev/test-studio/locales/no-NB/testStudio.ts b/dev/test-studio/locales/no-NB/testStudio.ts new file mode 100644 index 00000000000..dbd6413021c --- /dev/null +++ b/dev/test-studio/locales/no-NB/testStudio.ts @@ -0,0 +1,13 @@ +import {defineLanguageBundle} from 'sanity' +import {TestStudioTranslations} from '../types' +import {testStudioI18nNamespace} from '../en-US/testStudio' + +export const testStudioI18nNamespaceStrings: Partial = { + 'studio.logo.title': 'Norwegian logo', + 'structure.root.title': 'Innhold', +} + +export default defineLanguageBundle({ + namespace: testStudioI18nNamespace, + resources: testStudioI18nNamespaceStrings, +}) diff --git a/dev/test-studio/locales/types.ts b/dev/test-studio/locales/types.ts new file mode 100644 index 00000000000..89bea2d51e9 --- /dev/null +++ b/dev/test-studio/locales/types.ts @@ -0,0 +1,16 @@ +// eslint-disable-next-line import/no-unassigned-import +import 'sanity' +import {schemaI18nNamespaceStrings} from './en-US/schema' +import {testStudioI18nNamespace, testStudioI18nNamespaceStrings} from './en-US/testStudio' + +export type TestStudioTranslations = typeof testStudioI18nNamespaceStrings +export type TestStudioSchemaTranslations = typeof schemaI18nNamespaceStrings + +declare module 'sanity' { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + interface SchemaTranslations extends TestStudioSchemaTranslations {} + + interface SanityLanguageResources { + [testStudioI18nNamespace]: TestStudioTranslations + } +} diff --git a/dev/test-studio/plugins/async-translation/I18nLazyChild.tsx b/dev/test-studio/plugins/async-translation/I18nLazyChild.tsx new file mode 100644 index 00000000000..e99b6ecaf4d --- /dev/null +++ b/dev/test-studio/plugins/async-translation/I18nLazyChild.tsx @@ -0,0 +1,7 @@ +import React from 'react' +import {useTranslation} from 'sanity' + +export default function I18nLazyChild() { + const {t} = useTranslation('i18nTool') + return <>{t('childText')} +} diff --git a/dev/test-studio/plugins/async-translation/I18nLazyParent.tsx b/dev/test-studio/plugins/async-translation/I18nLazyParent.tsx new file mode 100644 index 00000000000..6ea452268ed --- /dev/null +++ b/dev/test-studio/plugins/async-translation/I18nLazyParent.tsx @@ -0,0 +1,38 @@ +import React, {lazy, Suspense} from 'react' +import {Card} from '@sanity/ui' +import {LanguageLoader, LanguageBundle, TranslationLoader, useTranslation} from 'sanity' + +const Child = lazy( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve(import('./I18nLazyChild')) + }, 2000) + }), +) + +const namespace = 'i18nTool' +const loader: LanguageLoader = async (lang) => { + return new Promise((resolve) => { + resolve({ + namespace: namespace, + resources: { + loadingChild: `${lang}: Loading lazy child...`, + childText: `${lang}: Child`, + }, + }) + }) +} + +export default function I18nLazyParent() { + const {t} = useTranslation(namespace) + return ( + + + {t('loadingChild')}}> + + + + + ) +} diff --git a/dev/test-studio/plugins/async-translation/plugin.tsx b/dev/test-studio/plugins/async-translation/plugin.tsx new file mode 100644 index 00000000000..2683eb77770 --- /dev/null +++ b/dev/test-studio/plugins/async-translation/plugin.tsx @@ -0,0 +1,15 @@ +import {definePlugin} from 'sanity' +import {EarthGlobeIcon} from '@sanity/icons' +import {lazy} from 'react' + +export const asyncTranslationTool = definePlugin({ + name: '@local/async-translation', + tools: [ + { + name: 'async-i18n', + title: 'I18n', + icon: EarthGlobeIcon, + component: lazy(() => import('./I18nLazyParent')), + }, + ], +}) diff --git a/dev/test-studio/sanity.config.ts b/dev/test-studio/sanity.config.ts index 12c708ed827..55886be8dfc 100644 --- a/dev/test-studio/sanity.config.ts +++ b/dev/test-studio/sanity.config.ts @@ -1,6 +1,6 @@ import {BookIcon} from '@sanity/icons' import {visionTool} from '@sanity/vision' -import {defineConfig, definePlugin} from 'sanity' +import {defineConfig, definePlugin, localizedLanguages} from 'sanity' import {deskTool} from 'sanity/desk' import {muxInput} from 'sanity-plugin-mux-input' import {assist} from '@sanity/assist' @@ -42,6 +42,7 @@ import {assistFieldActionGroup} from './fieldActions/assistFieldActionGroup' import {commentAction} from './fieldActions/commentFieldAction' import {customInspector} from './inspectors/custom' import {pasteAction} from './fieldActions/pasteAction' +import {asyncTranslationTool} from './plugins/async-translation/plugin' const sharedSettings = definePlugin({ name: 'sharedSettings', @@ -59,6 +60,18 @@ const sharedSettings = definePlugin({ logo: Branding, }, }, + + i18n: { + languages: (prev) => [localizedLanguages['no-NB'], ...prev], + languageLoaders: (prev) => { + return [ + ...prev, + (lang) => import(`./locales/${lang}/testStudio.ts`), + (lang) => import(`./locales/${lang}/schema.ts`), + ] + }, + }, + document: { actions: documentActions, inspectors: (prev, ctx) => { @@ -120,6 +133,7 @@ const sharedSettings = definePlugin({ muxInput({mp4_support: 'standard'}), presenceTool(), tsdoc(), + asyncTranslationTool(), ], }) @@ -131,6 +145,9 @@ export default defineConfig([ dataset: 'test', plugins: [sharedSettings()], basePath: '/test', + i18n: { + experimentalTranslateSchemas: true, + }, }, { name: 'tsdoc', diff --git a/dev/test-studio/schema/debug/i18nSchemaTranslation.ts b/dev/test-studio/schema/debug/i18nSchemaTranslation.ts new file mode 100644 index 00000000000..d2a1ba4896a --- /dev/null +++ b/dev/test-studio/schema/debug/i18nSchemaTranslation.ts @@ -0,0 +1,50 @@ +import {defineType} from 'sanity' +import {defineArrayMember, defineField} from '@sanity/types' + +export const i18nArray = defineType({ + type: 'array', + name: 'i18nArray', + title: 'i18n Array', + + of: [defineArrayMember({type: 'book'}), defineArrayMember({type: 'author'})], +}) + +export const i18nRef = defineType({ + type: 'reference', + name: 'i18nRef', + title: 'i18n ref', + + to: [{type: 'book'}, {type: 'author'}], +}) + +export const i18nDocument = defineType({ + type: 'document', + name: 'i18nDocument', + title: 'Debug: i18n document', + + fieldsets: [{name: 'fieldset', title: 'Fieldset'}], + groups: [{name: 'group', title: 'Group'}], + + fields: [ + defineField({ + type: 'string', + name: 'title', + title: 'Title', + description: 'Description', + }), + defineField({ + type: i18nArray.name, + name: 'array', + title: 'Array', + description: 'Description', + fieldset: 'fieldset', + }), + defineField({ + type: i18nRef.name, + name: 'ref', + title: 'Ref', + description: 'Description', + group: 'group', + }), + ], +}) diff --git a/dev/test-studio/schema/index.ts b/dev/test-studio/schema/index.ts index 3a15300cc8f..43ca4bf9e58 100644 --- a/dev/test-studio/schema/index.ts +++ b/dev/test-studio/schema/index.ts @@ -113,6 +113,7 @@ import {circularCrossDatasetReferenceTest} from './debug/circularCrossDatasetRef import {allNativeInputComponents} from './debug/allNativeInputComponents' import fieldGroupsWithFieldsets from './debug/fieldGroupsWithFieldsets' import ptReference from './debug/ptReference' +import {i18nArray, i18nDocument, i18nRef} from './debug/i18nSchemaTranslation' // @todo temporary, until code input is v3 compatible const codeInputType = { @@ -262,4 +263,7 @@ export const schemaTypes = [ allNativeInputComponents, ...v3docs.types, ...demos3d.types, + i18nDocument, + i18nArray, + i18nRef, ] diff --git a/dev/test-studio/schema/standard/portableText/allTheBellsAndWhistles.ts b/dev/test-studio/schema/standard/portableText/allTheBellsAndWhistles.ts index e8408c229c2..29941afb1d9 100644 --- a/dev/test-studio/schema/standard/portableText/allTheBellsAndWhistles.ts +++ b/dev/test-studio/schema/standard/portableText/allTheBellsAndWhistles.ts @@ -150,7 +150,7 @@ export const ptAllTheBellsAndWhistlesType = defineType({ }), ], components: { - preview: InfoBoxPreview, + preview: InfoBoxPreview as any, }, preview: { select: { diff --git a/dev/test-studio/structure/constants.ts b/dev/test-studio/structure/constants.ts index 264ae339150..8adeba1bf40 100644 --- a/dev/test-studio/structure/constants.ts +++ b/dev/test-studio/structure/constants.ts @@ -78,6 +78,7 @@ export const DEBUG_INPUT_TYPES = [ 'ptReference', 'virtualizationInObject', 'virtualizationDebug', + 'i18nDocument', ] export const CI_INPUT_TYPES = ['conditionalFieldset', 'validationCI', 'textsTest'] diff --git a/dev/test-studio/structure/resolveStructure.ts b/dev/test-studio/structure/resolveStructure.ts index 5df475b2a17..2c4bf5c6c81 100644 --- a/dev/test-studio/structure/resolveStructure.ts +++ b/dev/test-studio/structure/resolveStructure.ts @@ -30,9 +30,10 @@ import { import {delayValue} from './_helpers' import {typesInOptionGroup} from './groupByOption' -export const structure: StructureResolver = (S, {schema, documentStore}) => { +export const structure: StructureResolver = (S, {schema, documentStore, i18n}) => { + const {t} = i18n return S.list() - .title('Content') + .title(t('testStudio:structure.root.title' as const)) .items([ S.listItem() .title('Untitled repro') diff --git a/packages/sanity/package.json b/packages/sanity/package.json index f7ff00e5c33..b91a29942c6 100644 --- a/packages/sanity/package.json +++ b/packages/sanity/package.json @@ -184,6 +184,7 @@ "groq-js": "^1.1.12", "hashlru": "^2.3.0", "history": "^5.3.0", + "i18next": "^22.4.15", "import-fresh": "^3.3.0", "is-hotkey": "^0.1.6", "jsdom": "^20.0.0", @@ -208,6 +209,7 @@ "react-copy-to-clipboard": "^5.0.4", "react-fast-compare": "^3.2.0", "react-focus-lock": "^2.8.1", + "react-i18next": "^12.2.2", "react-is": "^18.2.0", "react-refractor": "^2.1.6", "react-rx": "^2.1.3", diff --git a/packages/sanity/src/core/config/configPropertyReducers.ts b/packages/sanity/src/core/config/configPropertyReducers.ts index b842b6f0c98..cf779cef9fb 100644 --- a/packages/sanity/src/core/config/configPropertyReducers.ts +++ b/packages/sanity/src/core/config/configPropertyReducers.ts @@ -1,16 +1,20 @@ import type {AssetSource, SchemaTypeDefinition} from '@sanity/types' +import type {InitOptions} from 'i18next' import {getPrintableType} from '../util/getPrintableType' import type {Template, TemplateItem} from '../templates' import type {DocumentActionComponent, DocumentBadgeComponent, DocumentInspector} from './document' import type { - DocumentLanguageFilterComponent, - DocumentLanguageFilterContext, AsyncConfigPropertyReducer, ConfigContext, ConfigPropertyReducer, DocumentActionsContext, DocumentBadgesContext, DocumentInspectorContext, + DocumentLanguageFilterComponent, + DocumentLanguageFilterContext, + I18nContext, + LanguageDefinition, + LanguageLoader, NewDocumentOptionsContext, ResolveProductionUrlContext, Tool, @@ -111,6 +115,50 @@ export const schemaTemplatesReducer: ConfigPropertyReducer = ( + prev, + {i18n}, + context, +) => { + const initOptions = i18n?.initOptions + if (!initOptions) return prev + if (typeof initOptions === 'function') return initOptions(prev, context) + + throw new Error( + `Expected \`i18n.initOptions\` to be a function, but received ${typeof initOptions}`, + ) +} + +export const i18nLoaderReducer: ConfigPropertyReducer = ( + prev, + {i18n}, + context, +) => { + const languageLoaders = i18n?.languageLoaders + if (!languageLoaders) return prev + if (typeof languageLoaders === 'function') return languageLoaders(prev, context) + if (Array.isArray(languageLoaders)) return [...prev, ...languageLoaders] + + throw new Error( + `Expected \`i18n.languageLoaders\` to be an array or a function, but received ${typeof languageLoaders}`, + ) +} + +export const i18nLangDefReducer: ConfigPropertyReducer = ( + prev, + {i18n}, + context, +) => { + const languages = i18n?.languages + if (!languages) return prev + if (typeof languages === 'function') return languages(prev, context) + if (Array.isArray(languages)) return [...prev, ...languages] + + throw new Error( + `Expected \`i18n.languages\` to be an array or a function, but received ${typeof languages}`, + ) +} + export const documentBadgesReducer: ConfigPropertyReducer< DocumentBadgeComponent[], DocumentBadgesContext diff --git a/packages/sanity/src/core/config/prepareConfig.ts b/packages/sanity/src/core/config/prepareConfig.ts index a2074f32099..ee307086375 100644 --- a/packages/sanity/src/core/config/prepareConfig.ts +++ b/packages/sanity/src/core/config/prepareConfig.ts @@ -14,8 +14,10 @@ import {EMPTY_ARRAY, isNonNullable} from '../util' import {validateWorkspaces} from '../studio' import {filterDefinitions} from '../studio/components/navbar/search/definitions/defaultFilters' import {operatorDefinitions} from '../studio/components/navbar/search/definitions/operators/defaultOperators' +import {prepareI18nSource} from '../i18n/i18nConfig' import type { Config, + I18nSource, MissingConfigFile, PreparedConfig, SingleWorkspace, @@ -141,6 +143,7 @@ export function prepareConfig( // TODO: consider using the `ConfigResolutionError` throw new SchemaError(schema) } + const i18n = prepareI18nSource(source, schema) const auth = getAuthStore(source) const source$ = auth.state.pipe( @@ -152,6 +155,7 @@ export function prepareConfig( schema, authenticated, auth, + i18n, }) }), shareReplay(1), @@ -164,6 +168,7 @@ export function prepareConfig( title: source.title || startCase(source.name), auth, schema, + i18n, source: source$, } }) @@ -176,6 +181,7 @@ export function prepareConfig( basePath: joinBasePath(rootPath, rootSource.basePath), dataset: rootSource.dataset, schema: resolvedSources[0].schema, + i18n: resolvedSources[0].i18n, icon: normalizeIcon(rootSource.icon, title, `${rootSource.projectId} ${rootSource.dataset}`), name: rootSource.name || 'default', projectId: rootSource.projectId, @@ -210,6 +216,7 @@ interface ResolveSourceOptions { currentUser: CurrentUser | null authenticated: boolean auth: AuthStore + i18n: I18nSource } function getBifurClient(client: SanityClient, auth: AuthStore) { @@ -229,6 +236,7 @@ function resolveSource({ schema, authenticated, auth, + i18n, }: ResolveSourceOptions): Source { const {dataset, projectId} = config const bifur = getBifurClient(client, auth) @@ -456,6 +464,7 @@ function resolveSource({ authenticated, templates, auth, + i18n, document: { actions: (partialContext) => resolveConfigProperty({ diff --git a/packages/sanity/src/core/config/types.ts b/packages/sanity/src/core/config/types.ts index 9d2c6b64f35..33948ef723d 100644 --- a/packages/sanity/src/core/config/types.ts +++ b/packages/sanity/src/core/config/types.ts @@ -11,6 +11,7 @@ import type { } from '@sanity/types' import type {ComponentType, ReactNode} from 'react' import type {Observable} from 'rxjs' +import type {i18n, InitOptions, TFunction, ResourceLanguage} from 'i18next' import type { BlockAnnotationProps, BlockProps, @@ -32,8 +33,8 @@ import type { DocumentActionComponent, DocumentBadgeComponent, DocumentFieldAction, - DocumentFieldActionsResolverContext, DocumentFieldActionsResolver, + DocumentFieldActionsResolverContext, DocumentInspector, } from './document' import type {Router, RouterState} from 'sanity/router' @@ -333,6 +334,126 @@ export type DocumentInspectorsResolver = ComposableOption< * @hidden * @beta */ +export interface I18nContext { + projectId: string + dataset: string +} + +/** + * @hidden + * @beta + */ +export interface LanguageBundle { + namespace: string + resources: ResourceLanguage + /** Should the resources be merged deeply (nested objects). Default: true */ + deep?: boolean + /** Should existing resource keys for the namespace be overwritten. Default: false */ + overwrite?: boolean +} + +/** @beta @hidden */ +export type I18nLanguagesOption = + | ((prev: LanguageDefinition[], context: I18nContext) => LanguageDefinition[]) + | LanguageDefinition[] + +/** @beta @hidden */ +export type I18nLanguageLoaderOption = + | ((prev: LanguageLoader[], context: I18nContext) => LanguageLoader[]) + | LanguageLoader[] + +/** @beta @hidden */ +export interface I18nPluginOptions { + /** + * Defines which languages should be available for user selection. + * Prev is initially `[{id: 'en-US', title: 'English (US)', icon: AmericanFlag }]` + * + * Language titles and icons can be changed by transforming the LanguageDefinition array values. + * + * User selected language + */ + languages?: I18nLanguagesOption + + /** + * Allows redefining the I18next init options before they are used. + * Invoked when a workspace is loaded + */ + initOptions?: (options: InitOptions, context: I18nContext) => InitOptions + + /** + * Defines language bundles that will be loaded lazily. + * + * ### Example + * + * ```ts + * + * ``` + * + */ + languageLoaders?: I18nLanguageLoaderOption + + /** + * When this is true, schema type title and descriptions will be translated. + * Configure a languageLoader that returns a language bundle for the `schema` namespace, + * with resource keys using the following convention: + * + * ## Keys for types + * + * - `.type-title` + * - `.type-description` + * - `..fieldset-title` + * - `..fieldset-description` + * - `..group-title` + * + * ## Keys for fields + * + * - `..field-title` + * - `..field-description` + * + * ## Keys for array items + * + * - `..item-title` + * + * ## Keys for reference types + * + * - `..ref-title` + * + * ## Caveats + * + * Enabling schema translations could adversely impact studio performance. + * Inline definitions for objects are not supported (nested types). + * + * ## Example LanguageBundle + * + *```ts + * // locales/en_US/schema.ts + * export default { + * namespace: 'schema', + * resources: { + * 'myDocumentType|title': 'Document type 'myDocumentType' will use this string as title wherever it is used', + * + * 'myDocumentType.text|title': 'Document field named 'text' will use this string as title' + * 'myDocumentType.text|description': 'Document field named 'text' will this string as description', + * }, + * } + *``` + * + * Now, in studio.config.ts: + * ```ts + * defineConfig({ + * i18n: { + * experimentalTranslateSchemas: true, + * languageLoaders: [ + * (lang) => import(`./locales/${lang}/schema.ts`), + * ] + * }, + * }) + * ``` + */ + experimentalTranslateSchemas?: boolean +} + +/** @beta */ export interface PluginOptions { name: string plugins?: PluginOptions[] @@ -345,6 +466,8 @@ export interface PluginOptions { studio?: { components?: StudioComponentsPluginOptions } + /** @beta @hidden */ + i18n?: I18nPluginOptions } /** @internal */ @@ -554,7 +677,7 @@ export interface Source { */ resolveNewDocumentOptions: (context: NewDocumentCreationContext) => InitialValueTemplateItem[] - /** @alpha */ + /** @beta @hidden */ unstable_languageFilter: ( props: PartialContext, ) => DocumentLanguageFilterComponent[] @@ -634,12 +757,15 @@ export interface Source { components?: StudioComponents } - /** @alpha */ + /** @beta @hidden */ search: { filters: SearchFilterDefinition[] operators: SearchOperatorDefinition[] } + /** @internal */ + i18n: I18nSource + /** @internal */ __internal: { bifur: BifurClient @@ -647,6 +773,29 @@ export interface Source { options: SourceOptions } } + +/** @beta @hidden */ +export interface LanguageDefinition { + id: string + title: string + icon?: ComponentType +} + +/** @beta @hidden */ +export type LanguageLoaderResult = Promise + +/** @beta @hidden */ +export type LanguageLoader = (language: string, context: {i18n: i18n}) => LanguageLoaderResult + +/** @internal */ +export interface I18nSource { + languages: LanguageDefinition[] + initOptions: InitOptions + languageLoaders: LanguageLoader[] + t: TFunction + i18next: i18n +} + /** @internal */ export interface WorkspaceSummary { type: 'workspace-summary' @@ -660,6 +809,7 @@ export interface WorkspaceSummary { dataset: string theme: StudioTheme schema: Schema + i18n: I18nSource /** * @internal * @deprecated not actually deprecated but don't use or you'll be fired @@ -672,6 +822,7 @@ export interface WorkspaceSummary { title: string auth: AuthStore schema: Schema + i18n: I18nSource source: Observable }> } diff --git a/packages/sanity/src/core/config/useConfigContextFromSource.ts b/packages/sanity/src/core/config/useConfigContextFromSource.ts index acfe994fe40..e3ade1b35b9 100644 --- a/packages/sanity/src/core/config/useConfigContextFromSource.ts +++ b/packages/sanity/src/core/config/useConfigContextFromSource.ts @@ -1,5 +1,5 @@ import {useMemo} from 'react' -import type {Source, ConfigContext} from './types' +import type {Source, ConfigContext, I18nSource} from './types' /** * Reduce a {@link Source} down to a {@link ConfigContext}, memoizing using `React.useMemo` @@ -8,11 +8,11 @@ import type {Source, ConfigContext} from './types' * @returns A config context containing only the defined properties of that interface * @internal */ -export function useConfigContextFromSource(source: Source): ConfigContext { - const {projectId, dataset, schema, currentUser, getClient} = source +export function useConfigContextFromSource(source: Source): ConfigContext & {i18n: I18nSource} { + const {projectId, dataset, schema, currentUser, getClient, i18n} = source return useMemo(() => { - return {projectId, dataset, schema, currentUser, getClient} - }, [projectId, dataset, schema, currentUser, getClient]) + return {projectId, dataset, schema, currentUser, getClient, i18n} + }, [projectId, dataset, schema, currentUser, getClient, i18n]) } /** @@ -22,7 +22,7 @@ export function useConfigContextFromSource(source: Source): ConfigContext { * @returns A config context containing only the defined properties of that interface * @internal */ -export function getConfigContextFromSource(source: Source): ConfigContext { - const {projectId, dataset, schema, currentUser, getClient} = source - return {projectId, dataset, schema, currentUser, getClient} +export function getConfigContextFromSource(source: Source): ConfigContext & {i18n: I18nSource} { + const {projectId, dataset, schema, currentUser, getClient, i18n} = source + return {projectId, dataset, schema, currentUser, getClient, i18n} } diff --git a/packages/sanity/src/core/hooks/useTimeAgo.ts b/packages/sanity/src/core/hooks/useTimeAgo.ts index ad0c1880e94..001daed1f57 100644 --- a/packages/sanity/src/core/hooks/useTimeAgo.ts +++ b/packages/sanity/src/core/hooks/useTimeAgo.ts @@ -9,7 +9,7 @@ import { differenceInYears, format, } from 'date-fns' -import pluralize from 'pluralize-esm' +import {useTranslation} from 'react-i18next' interface TimeSpec { timestamp: string @@ -29,7 +29,7 @@ export interface TimeAgoOpts { /** @internal */ export function useTimeAgo(time: Date | string, {minimal, agoSuffix}: TimeAgoOpts = {}): string { - const resolved = formatRelativeTime(time, {minimal, agoSuffix}) + const resolved = useFormatRelativeTime(time, {minimal, agoSuffix}) const [, forceUpdate] = useReducer((x) => x + 1, 0) @@ -59,7 +59,8 @@ export function useTimeAgo(time: Date | string, {minimal, agoSuffix}: TimeAgoOpt return resolved.timestamp } -function formatRelativeTime(date: Date | string, opts: TimeAgoOpts = {}): TimeSpec { +function useFormatRelativeTime(date: Date | string, opts: TimeAgoOpts = {}): TimeSpec { + const {t} = useTranslation() const parsedDate = date instanceof Date ? date : new Date(date) // Invalid date? Return empty timestamp and `null` as refresh interval, to save us from @@ -99,91 +100,102 @@ function formatRelativeTime(date: Date | string, opts: TimeAgoOpts = {}): TimeSp } const diffWeeks = differenceInWeeks(now, parsedDate) - const weekSuffix = pluralize('week', diffWeeks) if (diffWeeks) { if (opts.minimal) { return { - timestamp: opts.agoSuffix ? `${diffWeeks}w ago` : `${diffWeeks}w`, + timestamp: opts.agoSuffix + ? t('timeAgo.weeks.minimal.ago', {count: diffWeeks}) + : t('timeAgo.weeks.minimal', {count: diffWeeks}), refreshInterval: ONE_HOUR, } } return { - timestamp: opts.agoSuffix ? `${diffWeeks} ${weekSuffix} ago` : `${diffWeeks} ${weekSuffix}`, + timestamp: opts.agoSuffix + ? t('timeAgo.weeks.ago', {count: diffWeeks}) + : t('timeAgo.weeks', {count: diffWeeks}), refreshInterval: ONE_HOUR, } } const diffDays = differenceInDays(now, parsedDate) - const daysSuffix = pluralize('days', diffDays) if (diffDays) { if (opts.minimal) { - const daysSince = opts.agoSuffix ? `${diffDays}d ago` : `${diffDays}d` return { - timestamp: diffDays === 1 ? 'yesterday' : daysSince, + timestamp: opts.agoSuffix + ? t('timeAgo.days.minimal.ago', {count: diffDays}) + : t('timeAgo.days.minimal', {count: diffDays}), refreshInterval: ONE_HOUR, } } - const daysSince = opts.agoSuffix ? `${diffDays} ${daysSuffix} ago` : `${diffDays} ${daysSuffix}` return { - timestamp: diffDays === 1 ? 'yesterday' : daysSince, + timestamp: opts.agoSuffix + ? t('timeAgo.days.ago', {count: diffDays}) + : t('timeAgo.days', {count: diffDays}), refreshInterval: ONE_HOUR, } } const diffHours = differenceInHours(now, parsedDate) - const hoursSuffix = pluralize('hour', diffHours) if (diffHours) { if (opts.minimal) { return { - timestamp: opts.agoSuffix ? `${diffHours}h ago` : `${diffHours}h`, + timestamp: opts.agoSuffix + ? t('timeAgo.hours.minimal.ago', {count: diffHours}) + : t('timeAgo.hours.minimal', {count: diffHours}), refreshInterval: ONE_MINUTE, } } return { - timestamp: opts.agoSuffix ? `${diffHours} ${hoursSuffix} ago` : `${diffHours} ${hoursSuffix}`, + timestamp: opts.agoSuffix + ? t('timeAgo.hours.ago', {count: diffHours}) + : t('timeAgo.hours', {count: diffHours}), refreshInterval: ONE_MINUTE, } } const diffMins = differenceInMinutes(now, parsedDate) - const minsSuffix = pluralize('minute', diffMins) if (diffMins) { if (opts.minimal) { return { - timestamp: opts.agoSuffix ? `${diffMins}m ago` : `${diffMins}m`, + timestamp: opts.agoSuffix + ? t('timeAgo.minutes.minimal.ago', {count: diffMins}) + : t('timeAgo.minutes.minimal', {count: diffMins}), refreshInterval: TWENTY_SECONDS, } } return { - timestamp: opts.agoSuffix ? `${diffMins} ${minsSuffix} ago` : `${diffMins} ${minsSuffix}`, + timestamp: opts.agoSuffix + ? t('timeAgo.minutes.ago', {count: diffMins}) + : t('timeAgo.minutes', {count: diffMins}), refreshInterval: TWENTY_SECONDS, } } const diffSeconds = differenceInSeconds(now, parsedDate) - const secsSuffix = pluralize('second', diffSeconds) if (diffSeconds > 10) { if (opts.minimal) { return { - timestamp: opts.agoSuffix ? `${diffSeconds}s ago` : `${diffSeconds}s`, + timestamp: opts.agoSuffix + ? t('timeAgo.seconds.minimal.ago', {count: diffSeconds}) + : t('timeAgo.seconds.minimal', {count: diffSeconds}), refreshInterval: FIVE_SECONDS, } } return { timestamp: opts.agoSuffix - ? `${diffSeconds} ${secsSuffix} ago` - : `${diffSeconds} ${secsSuffix}`, + ? t('timeAgo.seconds.ago', {count: diffSeconds}) + : t('timeAgo.seconds', {count: diffSeconds}), refreshInterval: FIVE_SECONDS, } } - return {timestamp: 'just now', refreshInterval: FIVE_SECONDS} + return {timestamp: t('timeAgo.justNow'), refreshInterval: FIVE_SECONDS} } diff --git a/packages/sanity/src/core/i18n/components/I18nProvider.tsx b/packages/sanity/src/core/i18n/components/I18nProvider.tsx new file mode 100644 index 00000000000..ea3aa441025 --- /dev/null +++ b/packages/sanity/src/core/i18n/components/I18nProvider.tsx @@ -0,0 +1,64 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import {type i18n} from 'i18next' +// eslint-disable-next-line import/no-extraneous-dependencies +import {I18nextProvider} from 'react-i18next' +import React, {PropsWithChildren, startTransition, Suspense, useEffect, useState} from 'react' +import {useSource} from '../../studio' +import {LoadingScreen} from '../../studio/screens' +import {defaultI18nOptions} from '../i18nConfig' +import {useLoadedLanguages, useLoadLanguage} from '../i18nHooks' +import {defaultLanguage} from '../localizedLanguages' +import {LanguageProvider} from './LanguageContext' + +/** + * @internal + */ +// eslint-disable-next-line @typescript-eslint/ban-types +export function I18nProvider(props: PropsWithChildren<{}>) { + const {i18n} = useSource() + const {initOptions, languageLoaders} = i18n + const [i18nInstance, setI18nInstance] = useState() + const [language, setLanguage] = useState('unknown') + const setLangLoaded = useLoadedLanguages() + const loadLanguage = useLoadLanguage(setLangLoaded, languageLoaders) + + useEffect( + () => { + const lang = initOptions?.lng ?? defaultLanguage.id + const options = initOptions ?? defaultI18nOptions + let mounted = true + i18n.i18next.init(options).then(async () => { + if (!mounted) return + await loadLanguage(lang, i18n.i18next) + if (!mounted) return + startTransition(() => { + setI18nInstance(i18n.i18next) + setLanguage(lang) + }) + }) + return () => { + mounted = false + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [] /* intentionally empty: we only want this to fire once, to async init i18n initial instance */, + ) + + if (!i18nInstance) { + return + } + return ( + }> + + + {props.children} + + + + ) +} diff --git a/packages/sanity/src/core/i18n/components/LanguageContext.tsx b/packages/sanity/src/core/i18n/components/LanguageContext.tsx new file mode 100644 index 00000000000..9b6c31301ae --- /dev/null +++ b/packages/sanity/src/core/i18n/components/LanguageContext.tsx @@ -0,0 +1,67 @@ +import React, {createContext, PropsWithChildren, useContext, useMemo} from 'react' +import {type i18n} from 'i18next' +import {useSource, useWorkspace} from '../../studio' +import {storePreferredLang} from '../languageStore' + +/** + * @internal + */ +export interface LanguageContextValue { + changeLanguage: (lang: string) => Promise + language: string +} + +const LanguageContext = createContext(undefined) + +/** + * @internal + */ +export function useLanguage(): LanguageContextValue { + const context = useContext(LanguageContext) + if (!context) { + throw new Error( + 'SanityI18nContext value missing. Is this hook being used outside SanityI18nContext.Provider?', + ) + } + + return context +} + +/** + * @internal + */ +export interface LanguageProviderProps { + language: string + loadLanguage: (lang: string, instance: i18n) => Promise + setLanguage: (lang: string) => void + i18nInstance: i18n +} + +/** + * @internal + */ +export function LanguageProvider(props: PropsWithChildren) { + const {i18nInstance, setLanguage, loadLanguage, language, children} = props + const {projectId, name: sourceId} = useSource() + const sanityContext: LanguageContextValue = useMemo(() => { + return { + changeLanguage: async (lang: string) => { + if (!i18nInstance) { + return + } + await loadLanguage(lang, i18nInstance) + await i18nInstance.changeLanguage(lang) + storePreferredLang(projectId, sourceId, lang) + setLanguage(lang) + }, + language, + } + }, [i18nInstance, loadLanguage, language, setLanguage, projectId, sourceId]) + + /* Use language as key to force re-render of the whole studio, to update lazy getters*/ + return ( + + {children} + + ) +} diff --git a/packages/sanity/src/core/i18n/components/TranslationLoader.tsx b/packages/sanity/src/core/i18n/components/TranslationLoader.tsx new file mode 100644 index 00000000000..60ccda10764 --- /dev/null +++ b/packages/sanity/src/core/i18n/components/TranslationLoader.tsx @@ -0,0 +1,63 @@ +import React, {PropsWithChildren, useEffect, useState} from 'react' +import {LanguageLoader} from '../../config' +import {useSource} from '../../studio' +import {LoadingScreen} from '../../studio/screens' +import {runLanguageLoaders, useLoadedLanguages, useLoadLanguage} from '../i18nHooks' +import {useLanguage} from './LanguageContext' + +/** + * @alpha + */ +export interface TranslationProps { + /** + * Loader to run when the component is mounted. + * + * Note: + * The prop is frozen: changing it after initial render will have no effect. + * It is recommended to define the loader statically function outside any React component. + * + * Consider using a cache for the loader result. + */ + languageLoader: LanguageLoader +} + +/** + * Loads a language bundle after the studio has been initialized. + * This is useful for plugins which use lazy-loaded components that wish to load + * language bundles just-in-time, instead of on startup. + * + * Note: + * This component will run a provided language loader when mounted. + * Multiple instances of the same component will not share state. + * Loader function should cache any results to prevent unnecessary network traffic. + * + * @alpha + */ +export function TranslationLoader(props: PropsWithChildren) { + const {children, languageLoader} = props + // frozen dependency + const [languageLoaders] = useState(() => [languageLoader]) + + const {i18n} = useSource() + const {language} = useLanguage() + + const setLangLoaded = useLoadedLanguages() + const loadLanguage = useLoadLanguage(setLangLoaded, languageLoaders) + const [loading, setLoading] = useState(true) + + useEffect(() => { + if (!setLangLoaded(language)) { + setLoading(false) + return + } + setLoading(true) + + runLanguageLoaders(language, i18n.i18next, languageLoaders).catch((e) => console.error(e)) + }, [loadLanguage, i18n, language, setLangLoaded, setLoading, languageLoaders]) + + if (loading) { + return + } + + return {children} +} diff --git a/packages/sanity/src/core/i18n/defineHelpers.ts b/packages/sanity/src/core/i18n/defineHelpers.ts new file mode 100644 index 00000000000..9103e46f6d4 --- /dev/null +++ b/packages/sanity/src/core/i18n/defineHelpers.ts @@ -0,0 +1,8 @@ +import {typed} from '@sanity/types' +import type {LanguageBundle} from '../config' + +/** + * Pass-trough function that provides type safety when defining a LanguageBundle. + * @alpha + */ +export const defineLanguageBundle = typed diff --git a/packages/sanity/src/core/i18n/i18nConfig.ts b/packages/sanity/src/core/i18n/i18nConfig.ts new file mode 100644 index 00000000000..9dc59d8a02d --- /dev/null +++ b/packages/sanity/src/core/i18n/i18nConfig.ts @@ -0,0 +1,119 @@ +import i18nApi, {type i18n, type InitOptions} from 'i18next' +import {Schema} from '@sanity/types' +import {I18nSource, LanguageDefinition, LanguageLoader, SourceOptions} from '../config' +import {resolveConfigProperty} from '../config/resolveConfigProperty' +import { + i18nLangDefReducer, + i18nLoaderReducer, + i18nOptionsReducer, +} from '../config/configPropertyReducers' +import {studioI18nNamespaceStrings} from './locales/en-US/studio' +import {defaultLanguage} from './localizedLanguages' +import {getPreferredLang} from './languageStore' +import {schemaI18nNamespace, studioI18nNamespace} from './i18nNamespaces' +import {studioLocaleLoader} from './studioLocaleLoader' +import {i18nSchema} from './i18nSchema' + +export const defaultI18nOptions: InitOptions = { + partialBundledLanguages: true, + defaultNS: studioI18nNamespace, + lng: defaultLanguage.id, + fallbackLng: defaultLanguage.id, + resources: { + [defaultLanguage.id]: { + [studioI18nNamespace]: studioI18nNamespaceStrings, + [schemaI18nNamespace]: {}, + }, + }, + debug: false, + initImmediate: false, + + interpolation: { + escapeValue: false, // handled by i18next-react + }, + react: { + bindI18nStore: 'added', + }, +} + +export function getInitialI18nOptions( + projectId: string, + sourceId: string, + languages: LanguageDefinition[], +): InitOptions { + const langId = getPreferredLang(projectId, sourceId) + const preferredLang = languages.find((l) => l.id === langId) + const lng = preferredLang?.id ?? languages[0]?.id ?? defaultI18nOptions.lng + return { + ...defaultI18nOptions, + lng, + supportedLngs: languages.map((def) => def.id), + } +} + +export function prepareI18nSource(source: SourceOptions, schema: Schema): I18nSource { + const {projectId, dataset} = source + const i18nLanguages = resolveConfigProperty({ + config: source, + context: {projectId: projectId, dataset}, + propertyName: 'i18n', + reducer: i18nLangDefReducer, + initialValue: [defaultLanguage], + }) + + const i18nInitOptions = resolveConfigProperty({ + propertyName: 'i18n', + config: source, + context: {projectId, dataset}, + reducer: i18nOptionsReducer, + initialValue: getInitialI18nOptions(projectId, source.name, i18nLanguages), + }) + + const i18nLoaders = resolveConfigProperty({ + config: source, + context: {projectId, dataset}, + propertyName: 'i18n', + reducer: i18nLoaderReducer, + initialValue: [studioLocaleLoader], + }) + + const i18nSource = createI18nApi({ + languages: i18nLanguages, + initOptions: i18nInitOptions, + languageLoaders: i18nLoaders, + }) + + // no support for reducing this prop atm + if (source.i18n?.experimentalTranslateSchemas) { + i18nSchema(schema, i18nSource.i18next) + } + return i18nSource +} + +function createI18nApi({ + languages, + initOptions, + languageLoaders, +}: { + languages: LanguageDefinition[] + initOptions: InitOptions + languageLoaders: LanguageLoader[] +}): I18nSource { + // We start out with an uninitialized instance. + // The async init call happens in I18nProvider + let i18nInstance = i18nApi.createInstance() + return { + languages, + initOptions, + languageLoaders, + get t() { + return i18nInstance.t + }, + get i18next() { + return i18nInstance + }, + set i18next(newInstance: i18n) { + i18nInstance = newInstance + }, + } +} diff --git a/packages/sanity/src/core/i18n/i18nHooks.ts b/packages/sanity/src/core/i18n/i18nHooks.ts new file mode 100644 index 00000000000..47fb503fa0c --- /dev/null +++ b/packages/sanity/src/core/i18n/i18nHooks.ts @@ -0,0 +1,78 @@ +import {useCallback, useRef} from 'react' +import {i18n} from 'i18next' +import {LanguageBundle, LanguageLoader} from '../config' + +const NO_LANGS: string[] = [] + +function unwrapModule(maybeModule: LanguageBundle | {default: LanguageBundle}): LanguageBundle { + return 'default' in maybeModule ? maybeModule.default : maybeModule +} + +export function useLoadedLanguages(): (lang: string) => boolean { + const loadedLangs = useRef(NO_LANGS) + + return useCallback( + (lang: string) => { + if (loadedLangs.current.includes(lang)) { + return false + } + loadedLangs.current = [...loadedLangs.current, lang] + return true + }, + [loadedLangs], + ) +} + +function addLoaderResult( + lang: string, + instance: i18n, + loaderResult: LanguageBundle | {default: LanguageBundle} | undefined, +) { + if (!loaderResult) { + return + } + + // eslint-disable-next-line no-param-reassign + loaderResult = unwrapModule(loaderResult) + const bundleArray = Array.isArray(loaderResult) ? loaderResult : [loaderResult] + + bundleArray + .map((bundle) => unwrapModule(bundle)) + .forEach((bundle) => { + instance.addResourceBundle( + lang, + bundle.namespace, + bundle.resources, + bundle.deep ?? true, + bundle.overwrite ?? false, + ) + }) +} + +export function useLoadLanguage( + setLangLoaded: (lang: string) => boolean, + languageLoaders: LanguageLoader[] | undefined, +): (lang: string, instance: i18n) => Promise { + return useCallback( + async (lang: string, instance: i18n) => { + if (!setLangLoaded(lang) || !languageLoaders) { + return undefined + } + return runLanguageLoaders(lang, instance, languageLoaders).catch((e) => console.error(e)) + }, + [setLangLoaded, languageLoaders], + ) +} + +export async function runLanguageLoaders( + lang: string, + instance: i18n, + languageLoaders: LanguageLoader[], +): Promise { + const loadLangs = languageLoaders.map((loader) => + loader(lang, {i18n: instance}) + .then((loaderResult) => addLoaderResult(lang, instance, loaderResult)) + .catch((e) => console.error(e)), + ) + await Promise.all(loadLangs) +} diff --git a/packages/sanity/src/core/i18n/i18nNamespaces.ts b/packages/sanity/src/core/i18n/i18nNamespaces.ts new file mode 100644 index 00000000000..b977ba7dcd8 --- /dev/null +++ b/packages/sanity/src/core/i18n/i18nNamespaces.ts @@ -0,0 +1,8 @@ +/** @alpha */ +export const studioI18nNamespace = 'studio' as const + +/** + * By-convention namespace for schema translations. Use to just-in-time translate schema title and description. + * @alpha + * */ +export const schemaI18nNamespace = 'schema' as const diff --git a/packages/sanity/src/core/i18n/i18nSchema.ts b/packages/sanity/src/core/i18n/i18nSchema.ts new file mode 100644 index 00000000000..234d44f4858 --- /dev/null +++ b/packages/sanity/src/core/i18n/i18nSchema.ts @@ -0,0 +1,206 @@ +import {Fieldset, ObjectField, Schema, SchemaType} from '@sanity/types' +import {type i18n} from 'i18next' +import {schemaI18nNamespace} from './i18nNamespaces' + +/** + * @internal + This function mutates the schema. + Does not translate inline definitions (ie, inline nested arrays or objects) + */ +export function i18nSchema(schema: Schema, i18next: i18n): void { + function getI18nString(path: string, property: string, defaultValue?: string) { + if (!defaultValue) return defaultValue + + if (typeof defaultValue !== 'string') { + // value is probably a React component, don't translate it + return defaultValue + } + const i18nKey = `${schemaI18nNamespace}:${path}.${property}` + const hasTranslation = i18next.exists(i18nKey) + return hasTranslation ? i18next.t(i18nKey as any) : defaultValue + } + + function translateField(parentType: SchemaType, field: ObjectField) { + return { + ...field, + get type() { + return { + ...field.type, + get title() { + return getI18nString( + `${parentType.name}.${field.name}`, + 'field-title', + field.type.title, + ) + }, + get description() { + return getI18nString( + `${parentType.name}.${field.name}`, + 'field-description', + field.type.description, + ) + }, + } + }, + } + } + + function withExperimentalSearch(originalType: any, newType: SchemaType): SchemaType { + //__experimental_search is non-enumberable props + Object.defineProperty(newType, '__experimental_search', { + enumerable: false, + get() { + return originalType.__experimental_search + }, + set(value) { + // eslint-disable-next-line camelcase + originalType.__experimental_search = value + }, + }) + return newType + } + + schema._registry = Object.fromEntries( + Object.entries(schema._registry)?.map(([key, schemaType]) => [ + key, + { + ...schemaType, + get: () => { + const originalType = schemaType.get() + let parentType = { + ...originalType, + get title() { + return getI18nString(originalType.name, 'type-title', originalType.title) + }, + get description() { + return getI18nString(originalType.name, 'type-description', originalType.description) + }, + } + + if ('groups' in parentType) { + parentType = { + ...parentType, + get groups() { + return originalType.groups?.map((group: any) => { + return { + ...group, + get title() { + return getI18nString( + `${parentType.name}.${group.name}`, + 'group-title', + group.title, + ) + }, + get description() { + return getI18nString( + `${parentType.name}.${group.name}`, + 'group-description', + group.description, + ) + }, + } + }) + }, + } + } + + if ('fields' in parentType) { + return withExperimentalSearch(originalType, { + ...parentType, + get fields() { + return parentType.fields?.map((field: ObjectField) => + translateField(parentType, field), + ) + }, + get fieldsets() { + return parentType.fieldsets?.map((fieldset: Fieldset) => { + if (fieldset.single) { + return { + ...fieldset, + get field() { + return translateField(parentType, fieldset.field) + }, + } + } + return { + ...fieldset, + get title() { + return getI18nString( + `${parentType.name}.${fieldset.name}`, + 'fieldset-title', + fieldset.title, + ) + }, + get description() { + return getI18nString( + `${parentType.name}.${fieldset.name}`, + 'fieldset-description', + fieldset.description, + ) + }, + get fields() { + return parentType.fields?.map((field: ObjectField) => + translateField(parentType, field), + ) + }, + } + }) + }, + }) + } + if ('to' in parentType) { + return withExperimentalSearch(originalType, { + ...parentType, + get to() { + return parentType.to?.map((refType: SchemaType) => ({ + ...refType, + get title() { + return getI18nString( + `${parentType.name}.${refType.name}`, + 'ref-title', + refType.title, + ) + }, + get description() { + return getI18nString( + `${parentType.name}.${refType.name}`, + 'ref-description', + refType.description, + ) + }, + })) + }, + }) + } + + if ('of' in parentType) { + return withExperimentalSearch(originalType, { + ...parentType, + get of() { + return parentType.of?.map((arrayMember: SchemaType) => ({ + ...arrayMember, + get title() { + return getI18nString( + `${parentType.name}.${arrayMember.name}`, + 'item-title', + arrayMember.title, + ) + }, + get description() { + return getI18nString( + `${parentType.name}.${arrayMember.name}`, + 'item-description', + arrayMember.description, + ) + }, + })) + }, + }) + } + + return withExperimentalSearch(originalType, parentType) + }, + }, + ]), + ) +} diff --git a/packages/sanity/src/core/i18n/index.ts b/packages/sanity/src/core/i18n/index.ts new file mode 100644 index 00000000000..7b35096eac8 --- /dev/null +++ b/packages/sanity/src/core/i18n/index.ts @@ -0,0 +1,8 @@ +export {useTranslation} from 'react-i18next' + +export * from './components/I18nProvider' +export * from './components/TranslationLoader' +export * from './localizedLanguages' +export * from './i18nNamespaces' +export * from './defineHelpers' +export * from './locales/types' diff --git a/packages/sanity/src/core/i18n/languageStore.ts b/packages/sanity/src/core/i18n/languageStore.ts new file mode 100644 index 00000000000..aeb4ad71ca8 --- /dev/null +++ b/packages/sanity/src/core/i18n/languageStore.ts @@ -0,0 +1,24 @@ +const LOCAL_STORAGE_PREFIX = 'sanity-language' + +export function getPreferredLang(projectId: string, sourceId: string): string | undefined { + if (!hasLocalStorage) { + return undefined + } + const language = localStorage.getItem(itemId(projectId, sourceId)) + return language ?? undefined +} + +export function storePreferredLang(projectId: string, sourceId: string, lang: string): void { + if (!hasLocalStorage) { + return + } + localStorage.setItem(itemId(projectId, sourceId), lang) +} + +function itemId(projectId: string, workspaceId: string) { + return [LOCAL_STORAGE_PREFIX, projectId, workspaceId].join(':') +} + +function hasLocalStorage() { + return typeof localStorage !== 'undefined' +} diff --git a/packages/sanity/src/core/i18n/locales/en-US/studio.ts b/packages/sanity/src/core/i18n/locales/en-US/studio.ts new file mode 100644 index 00000000000..f5b42a62a20 --- /dev/null +++ b/packages/sanity/src/core/i18n/locales/en-US/studio.ts @@ -0,0 +1,71 @@ +import {defineLanguageBundle} from '../../defineHelpers' +import {studioI18nNamespace} from '../../i18nNamespaces' + +export const studioI18nNamespaceStrings = { + /** Placeholder text for the omnisearch input field */ + 'navbar.search.placeholder': 'Search', + + /* Relative time, just now */ + 'timeAgo.justNow': 'just now', + + /* Relative time, granularity: weeks*/ + 'timeAgo.weeks_one': '{{count}} week', + 'timeAgo.weeks_other': '{{count}} weeks', + /* Relative time, granularity: weeks, configured to show ago suffix*/ + 'timeAgo.weeks.ago_one': '{{count}} week ago', + 'timeAgo.weeks.ago_other': '{{count}} weeks ago', + /* Relative time, granularity: count, using a minimal format*/ + 'timeAgo.weeks.minimal': '{{count}}w', + /* Relative time, granularity: weeks, using a minimal format, configured to show ago suffix*/ + 'timeAgo.weeks.minimal.ago': '{{count}}w ago', + + /* Relative time, granularity: days*/ + 'timeAgo.days_one': 'yesterday', + 'timeAgo.days_other': '{{count}} days', + /* Relative time, granularity: days, configured to show ago suffix*/ + 'timeAgo.days.ago_one': 'yesterday', + 'timeAgo.days.ago_other': '{{count}} days ago', + /* Relative time, granularity: days, using a minimal format*/ + 'timeAgo.days.minimal_one': 'yesterday', + 'timeAgo.days.minimal_other': '{{count}}d', + /* Relative time, granularity: days, using a minimal format, configured to show ago suffix*/ + 'timeAgo.days.minimal.ago': '{{count}}d ago', + + /* Relative time, granularity: hours*/ + 'timeAgo.hours_one': '{{count}} hour', + 'timeAgo.hours_other': '{{count}} hours', + /* Relative time, granularity: hours, configured to show ago suffix*/ + 'timeAgo.hours.ago_one': '{{count}} hour ago', + 'timeAgo.hours.ago_other': '{{count}} hours ago', + /* Relative time, granularity: hours, using a minimal format*/ + 'timeAgo.hours.minimal': '{{count}}h', + /* Relative time, granularity: hours, using a minimal format, configured to show ago suffix*/ + 'timeAgo.hours.minimal.ago': '{{count}}h ago', + + /* Relative time, granularity: minutes*/ + 'timeAgo.minutes_one': '{{count}} minute', + 'timeAgo.minutes_other': '{{count}} minutes', + /* Relative time, granularity: minutes, configured to show ago suffix*/ + 'timeAgo.minutes.ago_one': '{{count}} minute ago', + 'timeAgo.minutes.ago_other': '{{count}} minutes ago', + /* Relative time, granularity: minutes, using a minimal format*/ + 'timeAgo.minutes.minimal': '{{count}m', + /* Relative time, granularity: minutes, using a minimal format, configured to show ago suffix*/ + 'timeAgo.minutes.minimal.ago': '{{count}}m ago', + + /* Relative time, granularity: seconds*/ + 'timeAgo.seconds_one': '{{count}} second', + 'timeAgo.seconds_other': '{{count}} seconds', + /* Relative time, granularity: seconds, configured to show ago suffix*/ + 'timeAgo.seconds.ago_one': '{{count}} minute ago', + 'timeAgo.seconds.ago_other': '{{count}} second ago', + /* Relative time, granularity: seconds, using a minimal format*/ + 'timeAgo.seconds.minimal': '{{count}m', + /* Relative time, granularity: seconds, using a minimal format, configured to show ago suffix*/ + 'timeAgo.seconds.minimal.ago': '{{count}}m ago', +} + +export default defineLanguageBundle({ + namespace: studioI18nNamespace, + resources: studioI18nNamespaceStrings, +}) diff --git a/packages/sanity/src/core/i18n/locales/no-NB/studio.ts b/packages/sanity/src/core/i18n/locales/no-NB/studio.ts new file mode 100644 index 00000000000..1f06fe22831 --- /dev/null +++ b/packages/sanity/src/core/i18n/locales/no-NB/studio.ts @@ -0,0 +1,72 @@ +import type {StudioTranslations} from '../types/studio' +import {defineLanguageBundle} from '../../defineHelpers' +import {studioI18nNamespace} from '../../i18nNamespaces' + +export const studioI18nNamespaceStrings: Partial = { + /** Placeholder text for the omnisearch input field */ + 'navbar.search.placeholder': 'Søk', + + /* Relative time, just now */ + 'timeAgo.justNow': 'akkurat nå', + + /* Relative time, granularity: weeks*/ + 'timeAgo.weeks_one': '{{count}} uke', + 'timeAgo.weeks_other': '{{count}} uker', + /* Relative time, granularity: weeks, configured to show ago suffix*/ + 'timeAgo.weeks.ago_one': '{{count}} uke siden', + 'timeAgo.weeks.ago_other': '{{count}} uker siden', + /* Relative time, granularity: count, using a minimal format*/ + 'timeAgo.weeks.minimal': '{{count}}u', + /* Relative time, granularity: weeks, using a minimal format, configured to show ago suffix*/ + 'timeAgo.weeks.minimal.ago': '{{count}}u siden', + + /* Relative time, granularity: days*/ + 'timeAgo.days_one': 'i går', + 'timeAgo.days_other': '{{count}} dager', + /* Relative time, granularity: days, configured to show ago suffix*/ + 'timeAgo.days.ago_one': 'i går', + 'timeAgo.days.ago_other': '{{count}} dager siden', + /* Relative time, granularity: days, using a minimal format*/ + 'timeAgo.days.minimal_one': 'i går', + 'timeAgo.days.minimal_other': '{{count}}d', + /* Relative time, granularity: days, using a minimal format, configured to show ago suffix*/ + 'timeAgo.days.minimal.ago': '{{count}}d siden', + + /* Relative time, granularity: hours*/ + 'timeAgo.hours_one': 'en time', + 'timeAgo.hours_other': '{{count}} timer', + /* Relative time, granularity: hours, configured to show ago suffix*/ + 'timeAgo.hours.ago_one': 'en time siden', + 'timeAgo.hours.ago_other': '{{count}} timer siden', + /* Relative time, granularity: hours, using a minimal format*/ + 'timeAgo.hours.minimal': '{{count}}t', + /* Relative time, granularity: hours, using a minimal format, configured to show ago suffix*/ + 'timeAgo.hours.minimal.ago': '{{count}}t siden', + + /* Relative time, granularity: minutes*/ + 'timeAgo.minutes_one': 'ett minutt', + 'timeAgo.minutes_other': '{{count}} minutter', + /* Relative time, granularity: minutes, configured to show ago suffix*/ + 'timeAgo.minutes.ago_one': 'ett minutt siden', + 'timeAgo.minutes.ago_other': '{{count}} minutter siden', + /* Relative time, granularity: minutes, using a minimal format*/ + 'timeAgo.minutes.minimal': '{{count}m', + /* Relative time, granularity: minutes, using a minimal format, configured to show ago suffix*/ + 'timeAgo.minutes.minimal.ago': '{{count}}m siden', + + /* Relative time, granularity: seconds*/ + 'timeAgo.seconds_one': 'ett sekund', + 'timeAgo.seconds_other': '{{count}} sekunder', + /* Relative time, granularity: seconds, configured to show ago suffix*/ + 'timeAgo.seconds.ago_one': 'ett sekund siden', + 'timeAgo.seconds.ago_other': '{{count}} second ago', + /* Relative time, granularity: seconds, using a minimal format*/ + 'timeAgo.seconds.minimal': '{{count}m', + /* Relative time, granularity: seconds, using a minimal format, configured to show ago suffix*/ + 'timeAgo.seconds.minimal.ago': '{{count}}m ago', +} + +export default defineLanguageBundle({ + namespace: studioI18nNamespace, + resources: studioI18nNamespaceStrings, +}) diff --git a/packages/sanity/src/core/i18n/locales/types/index.ts b/packages/sanity/src/core/i18n/locales/types/index.ts new file mode 100644 index 00000000000..2cef0486112 --- /dev/null +++ b/packages/sanity/src/core/i18n/locales/types/index.ts @@ -0,0 +1,2 @@ +export * from './studio' +export * from './schema' diff --git a/packages/sanity/src/core/i18n/locales/types/schema.ts b/packages/sanity/src/core/i18n/locales/types/schema.ts new file mode 100644 index 00000000000..2f4b1cc002d --- /dev/null +++ b/packages/sanity/src/core/i18n/locales/types/schema.ts @@ -0,0 +1,6 @@ +// interface available for extension +/** + * @alpha + */ +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface SchemaTranslations {} diff --git a/packages/sanity/src/core/i18n/locales/types/studio.ts b/packages/sanity/src/core/i18n/locales/types/studio.ts new file mode 100644 index 00000000000..84cfc7b593c --- /dev/null +++ b/packages/sanity/src/core/i18n/locales/types/studio.ts @@ -0,0 +1,6 @@ +import {studioI18nNamespaceStrings} from '../en-US/studio' + +/** + * @alpha + */ +export type StudioTranslations = typeof studioI18nNamespaceStrings diff --git a/packages/sanity/src/core/i18n/localizedLanguages.tsx b/packages/sanity/src/core/i18n/localizedLanguages.tsx new file mode 100644 index 00000000000..5cef06cb661 --- /dev/null +++ b/packages/sanity/src/core/i18n/localizedLanguages.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import {typed} from '@sanity/types' +import {LanguageDefinition} from '../config' + +/** + * Languages that the studio has available LanguageBundles for + * @alpha + */ +export const localizedLanguages = { + 'en-US': typed({ + id: 'en-US', + title: 'English (US)', + //FIXME windows does not support these unicode characters + icon: () => <>🇺🇸, + }), + 'no-NB': typed({ + id: 'no-NB', + title: 'Bokmål', + //FIXME windows does not support these unicode characters + icon: () => <>🇳🇴, + }), +} + +/** + * @alpha + */ +export const defaultLanguage: LanguageDefinition = localizedLanguages['en-US'] diff --git a/packages/sanity/src/core/i18n/studioLocaleLoader.ts b/packages/sanity/src/core/i18n/studioLocaleLoader.ts new file mode 100644 index 00000000000..116cd11fc8e --- /dev/null +++ b/packages/sanity/src/core/i18n/studioLocaleLoader.ts @@ -0,0 +1,16 @@ +import {LanguageLoader} from '../config' +import {defaultLanguage, localizedLanguages} from './localizedLanguages' + +const asyncStudioLocale = Object.values(localizedLanguages) + .filter( + //strings for default lang are loaded sync in i18n options + (lang) => lang.id !== defaultLanguage.id, + ) + .map((lang) => lang.id) + +export const studioLocaleLoader: LanguageLoader = async (lang) => { + if (!asyncStudioLocale.includes(lang)) { + return undefined + } + return import(`./locales/${lang}/studio.ts`) +} diff --git a/packages/sanity/src/core/index.ts b/packages/sanity/src/core/index.ts index a308d5434c6..9d19f17d2fe 100644 --- a/packages/sanity/src/core/index.ts +++ b/packages/sanity/src/core/index.ts @@ -27,3 +27,4 @@ export type { SearchSort, WeightedSearchOptions, } from './search' +export * from './i18n' diff --git a/packages/sanity/src/core/studio/StudioProvider.tsx b/packages/sanity/src/core/studio/StudioProvider.tsx index 74aeb431fe9..27eeca67ae3 100644 --- a/packages/sanity/src/core/studio/StudioProvider.tsx +++ b/packages/sanity/src/core/studio/StudioProvider.tsx @@ -25,6 +25,7 @@ import { } from './screens' import {WorkspaceLoader} from './workspaceLoader' import {WorkspacesProvider} from './workspaces' +import {I18nProvider} from '../i18n' Refractor.registerLanguage(bash) Refractor.registerLanguage(javascript) @@ -53,7 +54,9 @@ export function StudioProvider({ }: StudioProviderProps) { const _children = ( - {children} + + {children} + ) diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/PlaceholderSearchInput.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/PlaceholderSearchInput.tsx index ddcc7c6db09..2eaa7d211cd 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/PlaceholderSearchInput.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/PlaceholderSearchInput.tsx @@ -2,6 +2,7 @@ import {SearchIcon} from '@sanity/icons' import {Flex, KBD, TextInput} from '@sanity/ui' import React, {forwardRef, KeyboardEvent as ReactKeyboardEvent, Ref, useCallback} from 'react' +import {useTranslation} from 'react-i18next' import styled from 'styled-components' import {GLOBAL_SEARCH_KEY, GLOBAL_SEARCH_KEY_MODIFIER} from '../constants' import {useSearchState} from '../contexts/search/useSearchState' @@ -35,6 +36,8 @@ export const PlaceholderSearchInput = forwardRef(function DummyInput( state: {terms}, } = useSearchState() + const {t} = useTranslation() + const handleChange = useCallback( (event: ReactKeyboardEvent) => { dispatch({type: 'TERMS_QUERY_SET', query: event.currentTarget.value}) @@ -63,7 +66,7 @@ export const PlaceholderSearchInput = forwardRef(function DummyInput( onChange={handleChange} onClick={onOpen} onKeyDown={handleKeyDown} - placeholder="Search" + placeholder={t('navbar.search.placeholder') ?? undefined} radius={2} ref={ref} role="combobox" diff --git a/packages/sanity/src/core/studio/components/navbar/userMenu/LanguageMenu.tsx b/packages/sanity/src/core/studio/components/navbar/userMenu/LanguageMenu.tsx new file mode 100644 index 00000000000..f966d972010 --- /dev/null +++ b/packages/sanity/src/core/studio/components/navbar/userMenu/LanguageMenu.tsx @@ -0,0 +1,62 @@ +import {Box, Label, MenuDivider, MenuItem} from '@sanity/ui' +import React, {useCallback} from 'react' +import {useSource} from '../../../source' +import {LanguageDefinition} from '../../../../config' +import {useLanguage} from '../../../../i18n/components/LanguageContext' +import {useTranslation} from 'react-i18next' + +export function LanguageMenu() { + const {changeLanguage} = useLanguage() + const { + i18n: {language}, + } = useTranslation() + const {i18n} = useSource() + const languages = i18n.languages + if (!languages || languages.length < 2) { + return null + } + return ( + <> + + + + + + + {languages.map((lang) => ( + + ))} + + ) +} + +function LanguageItem(props: { + lang: LanguageDefinition + changeLanguage: (lang: string) => void + selectedLang: string +}) { + const {lang, changeLanguage, selectedLang} = props + const onClick = useCallback(() => changeLanguage(lang.id), [lang, changeLanguage]) + return ( + + {lang.id} + + ) + } + onClick={onClick} + text={lang.title} + /> + ) +} diff --git a/packages/sanity/src/core/studio/components/navbar/userMenu/UserMenu.tsx b/packages/sanity/src/core/studio/components/navbar/userMenu/UserMenu.tsx index ad170be1d69..ec61b8b9044 100644 --- a/packages/sanity/src/core/studio/components/navbar/userMenu/UserMenu.tsx +++ b/packages/sanity/src/core/studio/components/navbar/userMenu/UserMenu.tsx @@ -1,4 +1,4 @@ -import {LeaveIcon, ChevronDownIcon, CogIcon, CheckmarkIcon, UsersIcon} from '@sanity/icons' +import {CheckmarkIcon, ChevronDownIcon, CogIcon, LeaveIcon, UsersIcon} from '@sanity/icons' import { Box, Button, @@ -27,6 +27,7 @@ import { import {useWorkspace} from '../../../workspace' import {userHasRole} from '../../../../util/userHasRole' import {LoginProviderLogo} from './LoginProviderLogo' +import {LanguageMenu} from './LanguageMenu' const AVATAR_SIZE = 1 @@ -137,6 +138,7 @@ export function UserMenu() { {setScheme && } + {children} + return {children} } diff --git a/packages/sanity/src/desk/deskTool.ts b/packages/sanity/src/desk/deskTool.ts index 896451f37db..0d1bda30874 100644 --- a/packages/sanity/src/desk/deskTool.ts +++ b/packages/sanity/src/desk/deskTool.ts @@ -12,9 +12,12 @@ import {LiveEditBadge} from './documentBadges' import {getIntentState} from './getIntentState' import {router} from './router' import {DeskToolOptions} from './types' -import {validationInspector} from './panes/document/inspectors/validation' import {changesInspector} from './panes/document/inspectors/changes' -import {definePlugin} from 'sanity' +import {deskI18nNamespace} from './i18n/i18nNamespaces' +import {deskI18nNamespaceStrings} from './i18n/locales/en-US/desk' +import {deskLocaleLoader} from './i18n/deskLocaleLoader' +import {validationInspector} from './panes/document/inspectors/validation' +import {definePlugin, localizedLanguages} from 'sanity' const documentActions = [ PublishAction, @@ -114,4 +117,17 @@ export const deskTool = definePlugin((options) => ({ router, }, ], + i18n: { + initOptions: (initOptions) => ({ + ...initOptions, + resources: { + ...initOptions.resources, + [localizedLanguages['en-US'].id]: { + ...initOptions.resources?.['en-US'], + [deskI18nNamespace]: deskI18nNamespaceStrings, + }, + }, + }), + languageLoaders: [deskLocaleLoader], + }, })) diff --git a/packages/sanity/src/desk/documentActions/PublishAction.tsx b/packages/sanity/src/desk/documentActions/PublishAction.tsx index 8a07bd574c4..f013bd07a0d 100644 --- a/packages/sanity/src/desk/documentActions/PublishAction.tsx +++ b/packages/sanity/src/desk/documentActions/PublishAction.tsx @@ -1,8 +1,9 @@ import {CheckmarkIcon, PublishIcon} from '@sanity/icons' import {isValidationErrorMarker} from '@sanity/types' import React, {useCallback, useEffect, useState} from 'react' -import {TimeAgo} from '../components' +import type {TFunction} from 'i18next' import {useDocumentPane} from '../panes/document/useDocumentPane' +import {deskI18nNamespace, DeskTranslations} from '../i18n' import { DocumentActionComponent, InsufficientPermissionsMessage, @@ -11,30 +12,33 @@ import { useDocumentPairPermissions, useEditState, useSyncState, + useTimeAgo, + useTranslation, useValidationStatus, } from 'sanity' -const DISABLED_REASON_TITLE = { - LIVE_EDIT_ENABLED: 'Cannot publish since liveEdit is enabled for this document type', - ALREADY_PUBLISHED: 'Already published', - NO_CHANGES: 'No unpublished changes', - NOT_READY: 'Operation not ready', -} +const DISABLED_REASON_TITLE_KEY: Record = { + LIVE_EDIT_ENABLED: 'action.publish.liveEdit.publishDisabled', + ALREADY_PUBLISHED: 'action.publish.alreadyPublished.noTimeAgo.tooltip', + NO_CHANGES: 'action.publish.tooltip.noChanges', + NOT_READY: 'action.publish.disabled.notReady', +} as const function getDisabledReason( - reason: keyof typeof DISABLED_REASON_TITLE, + reason: keyof typeof DISABLED_REASON_TITLE_KEY, publishedAt: string | undefined, + t: TFunction, ) { if (reason === 'ALREADY_PUBLISHED' && publishedAt) { - return ( - <> - - Published - - - ) + return } - return DISABLED_REASON_TITLE[reason] + return t(DISABLED_REASON_TITLE_KEY[reason]) +} + +function AlreadyPublished({publishedAt}: {publishedAt: string}) { + const {t} = useTranslation(deskI18nNamespace) + const timeSincePublished = useTimeAgo(publishedAt) + return {t('action.publish.alreadyPublished.tooltip', {timeSincePublished})} } /** @internal */ @@ -47,6 +51,7 @@ export const PublishAction: DocumentActionComponent = (props) => { const syncState = useSyncState(id, type) const {changesOpen, onHistoryOpen, documentId, documentType} = useDocumentPane() const editState = useEditState(documentId, documentType) + const {t} = useTranslation(deskI18nNamespace) const revision = (editState?.draft || editState?.published || {})._rev @@ -65,9 +70,9 @@ export const PublishAction: DocumentActionComponent = (props) => { // eslint-disable-next-line no-nested-ternary const title = publish.disabled - ? getDisabledReason(publish.disabled, (published || {})._updatedAt) || '' + ? getDisabledReason(publish.disabled, (published || {})._updatedAt, t) || '' : hasValidationErrors - ? 'There are validation errors that need to be fixed before this document can be published' + ? t('action.publish.validationIssues.tooltip') : '' const hasDraft = Boolean(draft) @@ -138,9 +143,8 @@ export const PublishAction: DocumentActionComponent = (props) => { if (liveEdit) { return { tone: 'positive', - label: 'Publish', - title: - 'Live Edit is enabled for this content type and publishing happens automatically as you make changes', + label: t('action.publish.liveEdit.label'), + title: t('action.publish.liveEdit.tooltip'), disabled: true, } } @@ -174,16 +178,16 @@ export const PublishAction: DocumentActionComponent = (props) => { label: // eslint-disable-next-line no-nested-ternary publishState === 'published' - ? 'Published' + ? t('action.publish.published.label') : publishScheduled || publishState === 'publishing' - ? 'Publishing…' - : 'Publish', + ? t('action.publish.running.label') + : t('action.publish.draft.label'), // @todo: Implement loading state, to show a ` @@ -142,20 +148,21 @@ export function FieldChange( [ change, closeRevertChangesConfirmDialog, - readOnly, confirmRevertOpen, + fieldPath, + readOnly, DiffComponent, FieldWrapper, - hidden, handleRevertButtonMouseEnter, handleRevertButtonMouseLeave, handleRevertChanges, handleRevertChangesConfirm, + hidden, isComparingCurrent, isPermissionsLoading, - permissions, + permissions?.granted, revertHovered, - fieldPath, + t, ], ) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index ca0feaa8459..2a1a8af47ea 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -86,6 +86,14 @@ export const studioLocaleStrings = { /** Revert all label for revert button action - used on prompt button + review changes pane */ 'core.review-changes.revert-button-revert-all': `Revert all`, + + /** --- Review Changes: Field --- */ + + /** Revert changes prompt for field change */ + 'core.review-changes.revert-button-prompt': `Are you sure you want to revert the changes?`, + + /** Revert changes label for button for field change */ + 'core.review-changes.revert-button-change': `Revert change`, } /** From 1f412f4f97f1b9d3366ce9b8add0e719afeb6ac2 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Tue, 8 Aug 2023 14:00:32 +0200 Subject: [PATCH 039/163] feat(desk): add i18n primitives to GroupChange --- .../field/diff/components/GroupChange.tsx | 26 +++++++------------ .../sanity/src/core/i18n/bundles/studio.ts | 2 +- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/sanity/src/core/field/diff/components/GroupChange.tsx b/packages/sanity/src/core/field/diff/components/GroupChange.tsx index b9989de7b17..e375d8cd346 100644 --- a/packages/sanity/src/core/field/diff/components/GroupChange.tsx +++ b/packages/sanity/src/core/field/diff/components/GroupChange.tsx @@ -10,6 +10,7 @@ import {pathsAreEqual} from '../../paths' import {DiffContext} from '../contexts/DiffContext' import {useDocumentChange} from '../hooks' import {useDocumentPairPermissions} from '../../../store' +import {useTranslation} from '../../../i18n' import {ChangeBreadcrumb} from './ChangeBreadcrumb' import {ChangeResolver} from './ChangeResolver' import {RevertChangesButton} from './RevertChangesButton' @@ -23,16 +24,11 @@ export function GroupChange( hidden?: boolean } & React.HTMLAttributes, ): React.ReactElement | null { - const { - change: group, - readOnly, - hidden, - // 'data-revert-all-changes-hover': dataRevertAllChangesHover, - ...restProps - } = props + const {change: group, readOnly, hidden, ...restProps} = props const {titlePath, changes, path: groupPath} = group const {path: diffPath} = useContext(DiffContext) const {documentId, schemaType, FieldWrapper, rootDiff, isComparingCurrent} = useDocumentChange() + const {t} = useTranslation() const isPortableText = changes.every( (change) => isFieldChange(change) && isPTSchemaType(change.schemaType), @@ -74,9 +70,6 @@ export function GroupChange( as={GroupChangeContainer} data-revert-group-hover={isRevertButtonHovered ? '' : undefined} data-portable-text={isPortableText ? '' : undefined} - // data-revert-all-groups-hover={ - // restProps[] === '' ? '' : undefined - // } > {changes.map((change) => ( @@ -92,13 +85,13 @@ export function GroupChange( - Are you sure you want to revert the changes? + {t('core.review-changes.revert-button-prompt')} @@ -126,17 +119,18 @@ export function GroupChange( changes, closeRevertChangesConfirmDialog, confirmRevertOpen, - readOnly, group.fieldsetName, handleRevertChanges, handleRevertChangesConfirm, hidden, isComparingCurrent, - isRevertButtonHovered, isPermissionsLoading, isPortableText, - permissions, + isRevertButtonHovered, + permissions?.granted, + readOnly, revertButtonRef, + t, ], ) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index 2a1a8af47ea..e636c23cb61 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -87,7 +87,7 @@ export const studioLocaleStrings = { /** Revert all label for revert button action - used on prompt button + review changes pane */ 'core.review-changes.revert-button-revert-all': `Revert all`, - /** --- Review Changes: Field --- */ + /** --- Review Changes: Field + Group --- */ /** Revert changes prompt for field change */ 'core.review-changes.revert-button-prompt': `Are you sure you want to revert the changes?`, From c91c88879445567052a640b31aae2d482e9b76ca Mon Sep 17 00:00:00 2001 From: RitaDias Date: Tue, 8 Aug 2023 14:18:19 +0200 Subject: [PATCH 040/163] feat(desk): add i18n primitives to DiffTooltip --- .../sanity/src/core/field/diff/components/DiffTooltip.tsx | 4 +++- packages/sanity/src/core/i18n/bundles/studio.ts | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/sanity/src/core/field/diff/components/DiffTooltip.tsx b/packages/sanity/src/core/field/diff/components/DiffTooltip.tsx index c6cbe958a8e..aadcef4f53f 100644 --- a/packages/sanity/src/core/field/diff/components/DiffTooltip.tsx +++ b/packages/sanity/src/core/field/diff/components/DiffTooltip.tsx @@ -6,6 +6,7 @@ import {useTimeAgo} from '../../../hooks' import {useUser} from '../../../store' import {AnnotationDetails, Diff} from '../../types' import {getAnnotationAtPath, useAnnotationColor} from '../annotations' +import {useTranslation} from '../../../i18n' /** @internal */ export interface DiffTooltipProps extends TooltipProps { @@ -74,6 +75,7 @@ function AnnotationItem({annotation}: {annotation: AnnotationDetails}) { const [user] = useUser(author) const color = useAnnotationColor(annotation) const timeAgo = useTimeAgo(timestamp, {minimal: true}) + const {t} = useTranslation() return ( @@ -89,7 +91,7 @@ function AnnotationItem({annotation}: {annotation: AnnotationDetails}) { - {user ? user.displayName : 'Loading…'} + {user ? user.displayName : t('desk.review-changes.loading-author')} diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index e636c23cb61..0af17a323d7 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -87,6 +87,9 @@ export const studioLocaleStrings = { /** Revert all label for revert button action - used on prompt button + review changes pane */ 'core.review-changes.revert-button-revert-all': `Revert all`, + /** Loading author of change in the differences tooltip in the review changes pane */ + 'desk.review-changes.loading-author': 'Loading…', + /** --- Review Changes: Field + Group --- */ /** Revert changes prompt for field change */ From 2185f02ce298328bc51d4bdbd1abe837c1ebcd3c Mon Sep 17 00:00:00 2001 From: RitaDias Date: Tue, 8 Aug 2023 14:38:15 +0200 Subject: [PATCH 041/163] feat(desk): add i18n primitives to timelineMenu --- packages/sanity/src/desk/i18n/resources.ts | 19 +++++++++++++++ .../panes/document/timeline/timelineMenu.tsx | 24 +++++++++++-------- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/sanity/src/desk/i18n/resources.ts b/packages/sanity/src/desk/i18n/resources.ts index 2a984ccfe3a..47a909128f8 100644 --- a/packages/sanity/src/desk/i18n/resources.ts +++ b/packages/sanity/src/desk/i18n/resources.ts @@ -56,6 +56,25 @@ const deskLocaleStrings = { /** Loading changes in Review Changes Pane */ 'desk.review-changes.loading-changes': 'Loading changes', + + /** --- Timeline --- */ + + /** Error prompt when revision cannot be loaded */ + 'desk.timeline.unable-to-load-rev': 'Unable to load revision', + + /** Label for latest version for timeline menu dropdown */ + 'desk.timeline.latest-version': 'Latest version', + + /** Label for loading history */ + 'desk.timeline.loading-history': 'Loading history', + + /** Label for determining since which version the changes for timeline menu dropdown are showing. + * Receives the time label as a parameter. + */ + 'desk.timeline.since': 'Since: {{timeLabel}}', + + /** Label for missing change version for timeline menu dropdown are showing */ + 'desk.timeline.since-version-missing': 'Since: unknown version', } /** diff --git a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx index 996410dfcc5..0440b79a905 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx @@ -5,10 +5,11 @@ import {upperFirst} from 'lodash' import React, {useCallback, useMemo, useState} from 'react' import styled from 'styled-components' import {useDocumentPane} from '../useDocumentPane' +import {deskLocaleNamespace} from '../../../i18n' import {TimelineError} from './TimelineError' import {formatTimelineEventLabel} from './helpers' import {Timeline} from './timeline' -import {Chunk, useTimelineSelector} from 'sanity' +import {Chunk, useTimelineSelector, useTranslation} from 'sanity' interface TimelineMenuProps { chunk: Chunk | null @@ -22,8 +23,7 @@ const Root = styled(Popover)` ` export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { - const {setTimelineRange, setTimelineMode, timelineError, ready, timelineStore, isDeleted} = - useDocumentPane() + const {setTimelineRange, setTimelineMode, timelineError, ready, timelineStore} = useDocumentPane() const [open, setOpen] = useState(false) const [button, setButton] = useState(null) const [popover, setPopover] = useState(null) @@ -35,6 +35,8 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { const realRevChunk = useTimelineSelector(timelineStore, (state) => state.realRevChunk) const sinceTime = useTimelineSelector(timelineStore, (state) => state.sinceTime) + const {t} = useTranslation(deskLocaleNamespace) + const handleOpen = useCallback(() => { setTimelineMode(mode) setOpen(true) @@ -75,11 +77,11 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { closable: true, description: err.message, status: 'error', - title: 'Unable to load revision', + title: t('desk.timeline.unable-to-load-rev'), }) } }, - [setTimelineMode, setTimelineRange, timelineStore, toast], + [setTimelineMode, setTimelineRange, t, timelineStore, toast], ) const selectSince = useCallback( @@ -93,11 +95,11 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { closable: true, description: err.message, status: 'error', - title: 'Unable to load revision', + title: t('desk.timeline.unable-to-load-rev'), }) } }, - [setTimelineMode, setTimelineRange, timelineStore, toast], + [setTimelineMode, setTimelineRange, t, timelineStore, toast], ) const handleLoadMore = useCallback(() => { @@ -138,9 +140,11 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { const revLabel = chunk ? `${upperFirst(formatTimelineEventLabel(chunk.type))}: ${timeLabel}` - : 'Latest version' + : t('desk.timeline.latest-version') - const sinceLabel = chunk ? `Since: ${timeLabel}` : 'Since: unknown version' + const sinceLabel = chunk + ? t('desk.timeline.since', {timeLabel: timeLabel}) + : t('desk.timeline.since-version-missing') const buttonLabel = mode === 'rev' ? revLabel : sinceLabel @@ -164,7 +168,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { ref={setButton} selected={open} style={{maxWidth: '100%'}} - text={ready ? buttonLabel : 'Loading history'} + text={ready ? buttonLabel : t('desk.timeline.loading-history')} /> ) From 21204012679084b785497146ae12b402a378df82 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Tue, 8 Aug 2023 14:43:16 +0200 Subject: [PATCH 042/163] feat(desk): add i18n primitives to TimelineError --- packages/sanity/src/desk/i18n/resources.ts | 6 ++++++ .../src/desk/panes/document/timeline/TimelineError.tsx | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/sanity/src/desk/i18n/resources.ts b/packages/sanity/src/desk/i18n/resources.ts index 47a909128f8..2679c8e52cb 100644 --- a/packages/sanity/src/desk/i18n/resources.ts +++ b/packages/sanity/src/desk/i18n/resources.ts @@ -75,6 +75,12 @@ const deskLocaleStrings = { /** Label for missing change version for timeline menu dropdown are showing */ 'desk.timeline.since-version-missing': 'Since: unknown version', + + /** Title for error when the timeline for the given document can't be loaded */ + 'desk.timeline.error-title': 'An error occurred whilst retrieving document changes.', + + /** Description for error when the timeline for the given document can't be loaded */ + 'desk.timeline.error-description': 'Document history transactions have not been affected.', } /** diff --git a/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx b/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx index 59d77530b1e..67ef6e1569c 100644 --- a/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx @@ -1,9 +1,12 @@ import {ErrorOutlineIcon} from '@sanity/icons' import {Flex, Stack} from '@sanity/ui' import React from 'react' -import {TextWithTone} from 'sanity' +import {deskLocaleNamespace} from '../../../i18n' +import {TextWithTone, useTranslation} from 'sanity' export function TimelineError() { + const {t} = useTranslation(deskLocaleNamespace) + return ( @@ -11,10 +14,10 @@ export function TimelineError() { - An error occurred whilst retrieving document changes. + {t('desk.timeline.error-title')} - Document history transactions have not been affected. + {t('desk.timeline.error-description')} From afe77a0eda3cc81b55bc3b4d7a31979a8a558786 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Wed, 9 Aug 2023 10:53:24 +0200 Subject: [PATCH 043/163] feat(desk): add i18n primitives to timeline --- packages/sanity/src/desk/i18n/resources.ts | 7 +++++++ .../sanity/src/desk/panes/document/timeline/timeline.tsx | 9 +++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/sanity/src/desk/i18n/resources.ts b/packages/sanity/src/desk/i18n/resources.ts index 2679c8e52cb..a3e95a1020f 100644 --- a/packages/sanity/src/desk/i18n/resources.ts +++ b/packages/sanity/src/desk/i18n/resources.ts @@ -81,6 +81,13 @@ const deskLocaleStrings = { /** Description for error when the timeline for the given document can't be loaded */ 'desk.timeline.error-description': 'Document history transactions have not been affected.', + + /** Error title for when the document doesn't have history */ + 'desk.timeline.no-document-history-title': 'No document history', + + /** Error description for when the document doesn't have history */ + 'desk.timeline.no-document-history-description': + 'When changing the content of the document, the document versions will appear in this menu.', } /** diff --git a/packages/sanity/src/desk/panes/document/timeline/timeline.tsx b/packages/sanity/src/desk/panes/document/timeline/timeline.tsx index e1392ee1196..5c53af2d182 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timeline.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timeline.tsx @@ -1,8 +1,9 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react' import {Box, Flex, Spinner, Text} from '@sanity/ui' +import {deskLocaleNamespace} from '../../../i18n' import {TimelineItem} from './timelineItem' import {ListWrapper, Root, StackWrapper} from './timeline.styled' -import {Chunk, CommandList, CommandListRenderItemCallback} from 'sanity' +import {Chunk, CommandList, CommandListRenderItemCallback, useTranslation} from 'sanity' interface TimelineProps { chunks: Chunk[] @@ -24,6 +25,7 @@ export const Timeline = ({ firstChunk, }: TimelineProps) => { const [mounted, setMounted] = useState(false) + const {t} = useTranslation(deskLocaleNamespace) const filteredChunks = useMemo(() => { return chunks.filter((c) => { @@ -84,11 +86,10 @@ export const Timeline = ({ {filteredChunks.length === 0 && ( - No document history + {t('desk.timeline.no-document-history-title')} - When changing the content of the document, the document versions will appear in this - menu. + {t('desk.timeline.no-document-history-description')} )} From 9aa8234c096333c34a787a84f53b169507bdd75d Mon Sep 17 00:00:00 2001 From: RitaDias Date: Wed, 9 Aug 2023 11:10:44 +0200 Subject: [PATCH 044/163] feat(desk): add i18n primitives to timelineItem + removal of unused consts --- packages/sanity/src/desk/i18n/resources.ts | 15 +++++++++++++++ .../src/desk/panes/document/timeline/constants.ts | 11 ----------- .../src/desk/panes/document/timeline/helpers.ts | 6 +----- .../desk/panes/document/timeline/timelineItem.tsx | 11 +++++++---- .../desk/panes/document/timeline/timelineMenu.tsx | 3 +-- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/sanity/src/desk/i18n/resources.ts b/packages/sanity/src/desk/i18n/resources.ts index a3e95a1020f..4fd84de9485 100644 --- a/packages/sanity/src/desk/i18n/resources.ts +++ b/packages/sanity/src/desk/i18n/resources.ts @@ -88,6 +88,21 @@ const deskLocaleStrings = { /** Error description for when the document doesn't have history */ 'desk.timeline.no-document-history-description': 'When changing the content of the document, the document versions will appear in this menu.', + + /** --- Timeline constants --- */ + + /** Label for when the timeline item is the latest in the history */ + 'desk.timeline.latest': 'Latest', + + /** Consts used in the timeline item component (dropdown menu) - helpers */ + 'desk.timeline.create': 'created', + 'desk.timeline.delete': 'deleted', + 'desk.timeline.discardDraft': 'discarded draft', + 'desk.timeline.initial': 'created', + 'desk.timeline.editDraft': 'edited', + 'desk.timeline.editLive': 'live edited', + 'desk.timeline.publish': 'published', + 'desk.timeline.unpublish': 'unpublished', } /** diff --git a/packages/sanity/src/desk/panes/document/timeline/constants.ts b/packages/sanity/src/desk/panes/document/timeline/constants.ts index c7c2a2ede7f..7c6f1120e3f 100644 --- a/packages/sanity/src/desk/panes/document/timeline/constants.ts +++ b/packages/sanity/src/desk/panes/document/timeline/constants.ts @@ -8,17 +8,6 @@ import { AddCircleIcon, } from '@sanity/icons' -export const TIMELINE_LABELS: {[key: string]: string | undefined} = { - create: 'created', - delete: 'deleted', - discardDraft: 'discarded draft', - initial: 'created', - editDraft: 'edited', - editLive: 'live edited', - publish: 'published', - unpublish: 'unpublished', -} - export const TIMELINE_ICON_COMPONENTS: {[key: string]: IconComponent | undefined} = { create: AddCircleIcon, delete: TrashIcon, diff --git a/packages/sanity/src/desk/panes/document/timeline/helpers.ts b/packages/sanity/src/desk/panes/document/timeline/helpers.ts index 9fa42089c4d..51d91191c39 100644 --- a/packages/sanity/src/desk/panes/document/timeline/helpers.ts +++ b/packages/sanity/src/desk/panes/document/timeline/helpers.ts @@ -1,11 +1,7 @@ import {IconComponent} from '@sanity/icons' -import {TIMELINE_ICON_COMPONENTS, TIMELINE_LABELS} from './constants' +import {TIMELINE_ICON_COMPONENTS} from './constants' import {ChunkType} from 'sanity' -export function formatTimelineEventLabel(type: ChunkType): string | undefined { - return TIMELINE_LABELS[type] -} - export function getTimelineEventIconComponent(type: ChunkType): IconComponent | undefined { return TIMELINE_ICON_COMPONENTS[type] } diff --git a/packages/sanity/src/desk/panes/document/timeline/timelineItem.tsx b/packages/sanity/src/desk/panes/document/timeline/timelineItem.tsx index 09bdae7c8fe..6592d117ef0 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timelineItem.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timelineItem.tsx @@ -1,10 +1,11 @@ import React, {useCallback, createElement, useMemo} from 'react' import {Box, ButtonTone, Card, Flex, Label, Stack, Text} from '@sanity/ui' import {format} from 'date-fns' -import {formatTimelineEventLabel, getTimelineEventIconComponent} from './helpers' +import {deskLocaleNamespace} from '../../../i18n' +import {getTimelineEventIconComponent} from './helpers' import {UserAvatarStack} from './userAvatarStack' import {EventLabel, IconBox, IconWrapper, Root, TimestampBox} from './timelineItem.styled' -import {ChunkType, Chunk} from 'sanity' +import {ChunkType, Chunk, useTranslation} from 'sanity' const TIMELINE_ITEM_EVENT_TONE: Record = { initial: 'primary', @@ -39,6 +40,8 @@ export function TimelineItem({ timestamp, type, }: TimelineItemProps) { + const {t} = useTranslation(deskLocaleNamespace) + const iconComponent = getTimelineEventIconComponent(type) const authorUserIds = Array.from(chunk.authors) const isSelectable = type !== 'delete' @@ -93,14 +96,14 @@ export function TimelineItem({ tone={isSelected ? 'primary' : TIMELINE_ITEM_EVENT_TONE[chunk.type]} > )} - {formatTimelineEventLabel(type) || {type}} + {t(`desk.timeline.${type}`) || {type}} diff --git a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx index 0440b79a905..a4fe1b1f1ea 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx @@ -7,7 +7,6 @@ import styled from 'styled-components' import {useDocumentPane} from '../useDocumentPane' import {deskLocaleNamespace} from '../../../i18n' import {TimelineError} from './TimelineError' -import {formatTimelineEventLabel} from './helpers' import {Timeline} from './timeline' import {Chunk, useTimelineSelector, useTranslation} from 'sanity' @@ -139,7 +138,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { const timeLabel = useFormattedTimestamp(chunk?.endTimestamp || '') const revLabel = chunk - ? `${upperFirst(formatTimelineEventLabel(chunk.type))}: ${timeLabel}` + ? `${upperFirst(t(`desk.timeline.${chunk.type}`))}: ${timeLabel}` : t('desk.timeline.latest-version') const sinceLabel = chunk From 1616b0a02280c1f956097bab2c0f91c7449809c1 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 14 Aug 2023 08:41:55 +0200 Subject: [PATCH 045/163] refactor(desk): capitalisation on i18n for timeline --- packages/sanity/src/desk/i18n/resources.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/sanity/src/desk/i18n/resources.ts b/packages/sanity/src/desk/i18n/resources.ts index 4fd84de9485..49d076e8ad8 100644 --- a/packages/sanity/src/desk/i18n/resources.ts +++ b/packages/sanity/src/desk/i18n/resources.ts @@ -95,14 +95,14 @@ const deskLocaleStrings = { 'desk.timeline.latest': 'Latest', /** Consts used in the timeline item component (dropdown menu) - helpers */ - 'desk.timeline.create': 'created', - 'desk.timeline.delete': 'deleted', - 'desk.timeline.discardDraft': 'discarded draft', - 'desk.timeline.initial': 'created', - 'desk.timeline.editDraft': 'edited', - 'desk.timeline.editLive': 'live edited', - 'desk.timeline.publish': 'published', - 'desk.timeline.unpublish': 'unpublished', + 'desk.timeline.create': 'Created', + 'desk.timeline.delete': 'Deleted', + 'desk.timeline.discardDraft': 'Discarded draft', + 'desk.timeline.initial': 'Created', + 'desk.timeline.editDraft': 'Edited', + 'desk.timeline.editLive': 'Live edited', + 'desk.timeline.publish': 'Published', + 'desk.timeline.unpublish': 'Unpublished', } /** From 71e27e7395f07721d806acfaeda898e7a0e7795b Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 14 Aug 2023 08:53:52 +0200 Subject: [PATCH 046/163] refactor(desk): remove unneeded method for revLabel --- .../sanity/src/desk/panes/document/timeline/timelineMenu.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx index a4fe1b1f1ea..e52ad425a07 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx @@ -137,9 +137,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { const timeLabel = useFormattedTimestamp(chunk?.endTimestamp || '') - const revLabel = chunk - ? `${upperFirst(t(`desk.timeline.${chunk.type}`))}: ${timeLabel}` - : t('desk.timeline.latest-version') + const revLabel = chunk ? t(`desk.timeline.${chunk.type}`) : t('desk.timeline.latest-version') const sinceLabel = chunk ? t('desk.timeline.since', {timeLabel: timeLabel}) From b89f2d9c4a5afddfe17d899ec2791843ae3016d9 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 14 Aug 2023 09:22:51 +0200 Subject: [PATCH 047/163] refactor(desk): remove unused imports + rely on i18n translate for time formatting - timelineMenu --- packages/sanity/src/desk/i18n/resources.ts | 2 +- .../panes/document/timeline/timelineMenu.tsx | 21 ++++++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/sanity/src/desk/i18n/resources.ts b/packages/sanity/src/desk/i18n/resources.ts index 49d076e8ad8..8c3ca4c7d4b 100644 --- a/packages/sanity/src/desk/i18n/resources.ts +++ b/packages/sanity/src/desk/i18n/resources.ts @@ -71,7 +71,7 @@ const deskLocaleStrings = { /** Label for determining since which version the changes for timeline menu dropdown are showing. * Receives the time label as a parameter. */ - 'desk.timeline.since': 'Since: {{timeLabel}}', + 'desk.timeline.since': 'Since: {{timestamp, datetime}}', /** Label for missing change version for timeline menu dropdown are showing */ 'desk.timeline.since-version-missing': 'Since: unknown version', diff --git a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx index e52ad425a07..986970582b5 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx @@ -1,7 +1,5 @@ import {SelectIcon} from '@sanity/icons' import {Button, Placement, Popover, useClickOutside, useGlobalKeyDown, useToast} from '@sanity/ui' -import {format} from 'date-fns' -import {upperFirst} from 'lodash' import React, {useCallback, useMemo, useState} from 'react' import styled from 'styled-components' import {useDocumentPane} from '../useDocumentPane' @@ -135,12 +133,15 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { ) - const timeLabel = useFormattedTimestamp(chunk?.endTimestamp || '') - const revLabel = chunk ? t(`desk.timeline.${chunk.type}`) : t('desk.timeline.latest-version') const sinceLabel = chunk - ? t('desk.timeline.since', {timeLabel: timeLabel}) + ? t('desk.timeline.since', { + timestamp: new Date(chunk?.endTimestamp), + formatParams: { + timestamp: {dateStyle: 'medium', timeStyle: 'short'}, + }, + }) : t('desk.timeline.since-version-missing') const buttonLabel = mode === 'rev' ? revLabel : sinceLabel @@ -170,13 +171,3 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { ) } - -export function useFormattedTimestamp(time: string): string { - const formatted = useMemo(() => { - const parsedDate = time ? new Date(time) : new Date() - const formattedDate = format(parsedDate, 'MMM d, yyyy, hh:mm a') - return formattedDate - }, [time]) - - return formatted -} From fcc0fc5c4562a222838d449ae57c8ba14aacf0f4 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 14 Aug 2023 09:34:48 +0200 Subject: [PATCH 048/163] refactor(desk): remove "desk" prefix from resources --- packages/sanity/src/desk/i18n/resources.ts | 44 +++++++++---------- .../inspectors/changes/ChangesInspector.tsx | 8 ++-- .../inspectors/changes/LoadingContent.tsx | 2 +- .../panes/document/timeline/TimelineError.tsx | 4 +- .../desk/panes/document/timeline/timeline.tsx | 4 +- .../panes/document/timeline/timelineItem.tsx | 4 +- .../panes/document/timeline/timelineMenu.tsx | 12 ++--- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/packages/sanity/src/desk/i18n/resources.ts b/packages/sanity/src/desk/i18n/resources.ts index 8c3ca4c7d4b..39854a8ec79 100644 --- a/packages/sanity/src/desk/i18n/resources.ts +++ b/packages/sanity/src/desk/i18n/resources.ts @@ -46,63 +46,63 @@ const deskLocaleStrings = { /** --- Review Changes --- */ /** Title for the Review Changes pane */ - 'desk.review-changes.title': 'Review changes', + 'review-changes.title': 'Review changes', /** Label for the close button label in Review Changes pane */ - 'desk.review-changes.close-label': 'Close review changes', + 'review-changes.close-label': 'Close review changes', /** Label and text for differences tooltip that indicates the authors of the changes */ - 'desk.review-changes.changes-by-author': 'Changes by', + 'review-changes.changes-by-author': 'Changes by', /** Loading changes in Review Changes Pane */ - 'desk.review-changes.loading-changes': 'Loading changes', + 'review-changes.loading-changes': 'Loading changes', /** --- Timeline --- */ /** Error prompt when revision cannot be loaded */ - 'desk.timeline.unable-to-load-rev': 'Unable to load revision', + 'timeline.unable-to-load-rev': 'Unable to load revision', /** Label for latest version for timeline menu dropdown */ - 'desk.timeline.latest-version': 'Latest version', + 'timeline.latest-version': 'Latest version', /** Label for loading history */ - 'desk.timeline.loading-history': 'Loading history', + 'timeline.loading-history': 'Loading history', /** Label for determining since which version the changes for timeline menu dropdown are showing. * Receives the time label as a parameter. */ - 'desk.timeline.since': 'Since: {{timestamp, datetime}}', + 'timeline.since': 'Since: {{timestamp, datetime}}', /** Label for missing change version for timeline menu dropdown are showing */ - 'desk.timeline.since-version-missing': 'Since: unknown version', + 'timeline.since-version-missing': 'Since: unknown version', /** Title for error when the timeline for the given document can't be loaded */ - 'desk.timeline.error-title': 'An error occurred whilst retrieving document changes.', + 'timeline.error-title': 'An error occurred whilst retrieving document changes.', /** Description for error when the timeline for the given document can't be loaded */ - 'desk.timeline.error-description': 'Document history transactions have not been affected.', + 'timeline.error-description': 'Document history transactions have not been affected.', /** Error title for when the document doesn't have history */ - 'desk.timeline.no-document-history-title': 'No document history', + 'timeline.no-document-history-title': 'No document history', /** Error description for when the document doesn't have history */ - 'desk.timeline.no-document-history-description': + 'timeline.no-document-history-description': 'When changing the content of the document, the document versions will appear in this menu.', /** --- Timeline constants --- */ /** Label for when the timeline item is the latest in the history */ - 'desk.timeline.latest': 'Latest', + 'timeline.latest': 'Latest', /** Consts used in the timeline item component (dropdown menu) - helpers */ - 'desk.timeline.create': 'Created', - 'desk.timeline.delete': 'Deleted', - 'desk.timeline.discardDraft': 'Discarded draft', - 'desk.timeline.initial': 'Created', - 'desk.timeline.editDraft': 'Edited', - 'desk.timeline.editLive': 'Live edited', - 'desk.timeline.publish': 'Published', - 'desk.timeline.unpublish': 'Unpublished', + 'timeline.create': 'Created', + 'timeline.delete': 'Deleted', + 'timeline.discardDraft': 'Discarded draft', + 'timeline.initial': 'Created', + 'timeline.editDraft': 'Edited', + 'timeline.editLive': 'Live edited', + 'timeline.publish': 'Published', + 'timeline.unpublish': 'Unpublished', } /** diff --git a/packages/sanity/src/desk/panes/document/inspectors/changes/ChangesInspector.tsx b/packages/sanity/src/desk/panes/document/inspectors/changes/ChangesInspector.tsx index 8df3de13434..e533190cef5 100644 --- a/packages/sanity/src/desk/panes/document/inspectors/changes/ChangesInspector.tsx +++ b/packages/sanity/src/desk/panes/document/inspectors/changes/ChangesInspector.tsx @@ -66,10 +66,10 @@ export function ChangesInspector(props: DocumentInspectorProps): ReactElement { @@ -79,10 +79,10 @@ export function ChangesInspector(props: DocumentInspectorProps): ReactElement { - + {changeAnnotations.map(({author}) => ( ))} diff --git a/packages/sanity/src/desk/panes/document/inspectors/changes/LoadingContent.tsx b/packages/sanity/src/desk/panes/document/inspectors/changes/LoadingContent.tsx index 3f2f5a7c2af..fb01c2d1da3 100644 --- a/packages/sanity/src/desk/panes/document/inspectors/changes/LoadingContent.tsx +++ b/packages/sanity/src/desk/panes/document/inspectors/changes/LoadingContent.tsx @@ -13,7 +13,7 @@ export function LoadingContent() { - {t('desk.review-changes.loading-changes')} + {t('review-changes.loading-changes')} diff --git a/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx b/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx index 67ef6e1569c..5a4dde48196 100644 --- a/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx @@ -14,10 +14,10 @@ export function TimelineError() { - {t('desk.timeline.error-title')} + {t('timeline.error-title')} - {t('desk.timeline.error-description')} + {t('timeline.error-description')} diff --git a/packages/sanity/src/desk/panes/document/timeline/timeline.tsx b/packages/sanity/src/desk/panes/document/timeline/timeline.tsx index 5c53af2d182..376f334245f 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timeline.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timeline.tsx @@ -86,10 +86,10 @@ export const Timeline = ({ {filteredChunks.length === 0 && ( - {t('desk.timeline.no-document-history-title')} + {t('timeline.no-document-history-title')} - {t('desk.timeline.no-document-history-description')} + {t('timeline.no-document-history-description')} )} diff --git a/packages/sanity/src/desk/panes/document/timeline/timelineItem.tsx b/packages/sanity/src/desk/panes/document/timeline/timelineItem.tsx index 6592d117ef0..72eaa02867a 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timelineItem.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timelineItem.tsx @@ -96,14 +96,14 @@ export function TimelineItem({ tone={isSelected ? 'primary' : TIMELINE_ITEM_EVENT_TONE[chunk.type]} > )} - {t(`desk.timeline.${type}`) || {type}} + {t(`timeline.${type}`) || {type}} diff --git a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx index 986970582b5..a0d7ecf16b6 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx @@ -74,7 +74,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { closable: true, description: err.message, status: 'error', - title: t('desk.timeline.unable-to-load-rev'), + title: t('timeline.unable-to-load-rev'), }) } }, @@ -92,7 +92,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { closable: true, description: err.message, status: 'error', - title: t('desk.timeline.unable-to-load-rev'), + title: t('timeline.unable-to-load-rev'), }) } }, @@ -133,16 +133,16 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { ) - const revLabel = chunk ? t(`desk.timeline.${chunk.type}`) : t('desk.timeline.latest-version') + const revLabel = chunk ? t(`timeline.${chunk.type}`) : t('timeline.latest-version') const sinceLabel = chunk - ? t('desk.timeline.since', { + ? t('timeline.since', { timestamp: new Date(chunk?.endTimestamp), formatParams: { timestamp: {dateStyle: 'medium', timeStyle: 'short'}, }, }) - : t('desk.timeline.since-version-missing') + : t('timeline.since-version-missing') const buttonLabel = mode === 'rev' ? revLabel : sinceLabel @@ -166,7 +166,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { ref={setButton} selected={open} style={{maxWidth: '100%'}} - text={ready ? buttonLabel : t('desk.timeline.loading-history')} + text={ready ? buttonLabel : t('timeline.loading-history')} /> ) From c4bdcff576eef27cbe6c006fa362051c9924fc7c Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 14 Aug 2023 09:37:29 +0200 Subject: [PATCH 049/163] refactor(desk): remove "core" prefix from studio --- .../core/field/diff/components/ChangeList.tsx | 8 ++++---- .../core/field/diff/components/DiffTooltip.tsx | 2 +- .../core/field/diff/components/FieldChange.tsx | 10 +++------- .../core/field/diff/components/GroupChange.tsx | 6 +++--- .../src/core/field/diff/components/NoChanges.tsx | 4 ++-- packages/sanity/src/core/i18n/bundles/studio.ts | 16 ++++++++-------- 6 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/sanity/src/core/field/diff/components/ChangeList.tsx b/packages/sanity/src/core/field/diff/components/ChangeList.tsx index c1a5e2be28c..de15d405704 100644 --- a/packages/sanity/src/core/field/diff/components/ChangeList.tsx +++ b/packages/sanity/src/core/field/diff/components/ChangeList.tsx @@ -118,16 +118,16 @@ export function ChangeList({diff, fields, schemaType}: ChangeListProps): React.R - {t('core.review-changes.revert-all-button-prompt', {count: changes.length})} + {t('review-changes.revert-all-button-prompt', {count: changes.length})} diff --git a/packages/sanity/src/core/field/diff/components/GroupChange.tsx b/packages/sanity/src/core/field/diff/components/GroupChange.tsx index e375d8cd346..8b5aae11a73 100644 --- a/packages/sanity/src/core/field/diff/components/GroupChange.tsx +++ b/packages/sanity/src/core/field/diff/components/GroupChange.tsx @@ -85,13 +85,13 @@ export function GroupChange( - {t('core.review-changes.revert-button-prompt')} + {t('review-changes.revert-button-prompt')} diff --git a/packages/sanity/src/core/field/diff/components/NoChanges.tsx b/packages/sanity/src/core/field/diff/components/NoChanges.tsx index ef86dd75701..b05dc2a78b2 100644 --- a/packages/sanity/src/core/field/diff/components/NoChanges.tsx +++ b/packages/sanity/src/core/field/diff/components/NoChanges.tsx @@ -8,10 +8,10 @@ export function NoChanges() { return ( - {t('core.review-changes.no-changes-title')} + {t('review-changes.no-changes-title')} - {t('core.review-changes.no-changes-description')} + {t('review-changes.no-changes-description')} ) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index 0af17a323d7..c9f968f8128 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -72,31 +72,31 @@ export const studioLocaleStrings = { /** --- Review Changes --- */ /** No Changes title in the Review Changes pane */ - 'core.review-changes.no-changes-title': 'There are no changes', + 'review-changes.no-changes-title': 'There are no changes', /** No Changes description in the Review Changes pane */ - 'core.review-changes.no-changes-description': + 'review-changes.no-changes-description': 'Edit the document or select an older version in the timeline to see a list of changes appear in this panel.', /** Prompt for reverting all changes in document in Review Changes pane */ - 'core.review-changes.revert-all-button-prompt': `Are you sure you want to revert all {{count}} changes?`, + 'review-changes.revert-all-button-prompt': `Are you sure you want to revert all {{count}} changes?`, /** Cancel label for revert button prompt action */ - 'core.review-changes.revert-button-cancel': `Cancel`, + 'review-changes.revert-button-cancel': `Cancel`, /** Revert all label for revert button action - used on prompt button + review changes pane */ - 'core.review-changes.revert-button-revert-all': `Revert all`, + 'review-changes.revert-button-revert-all': `Revert all`, /** Loading author of change in the differences tooltip in the review changes pane */ - 'desk.review-changes.loading-author': 'Loading…', + 'review-changes.loading-author': 'Loading…', /** --- Review Changes: Field + Group --- */ /** Revert changes prompt for field change */ - 'core.review-changes.revert-button-prompt': `Are you sure you want to revert the changes?`, + 'review-changes.revert-button-prompt': `Are you sure you want to revert the changes?`, /** Revert changes label for button for field change */ - 'core.review-changes.revert-button-change': `Revert change`, + 'review-changes.revert-button-change': `Revert change`, } /** From 0119f72ca1d6953b282193f2ab4d3421438a3fbf Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 14 Aug 2023 09:56:54 +0200 Subject: [PATCH 050/163] refactor(core): add plural to diff "revert changes" --- .../src/core/field/diff/components/RevertChangesButton.tsx | 6 +++++- packages/sanity/src/core/i18n/bundles/studio.ts | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx b/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx index c40d4ea8d9d..ee8c6a1e06c 100644 --- a/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx +++ b/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx @@ -1,6 +1,7 @@ import {RevertIcon} from '@sanity/icons' import {Button, ButtonProps} from '@sanity/ui' import React, {forwardRef} from 'react' +import {useTranslation} from 'react-i18next' import styled from 'styled-components' const Root = styled(Button)` @@ -30,12 +31,15 @@ export const RevertChangesButton = forwardRef(function RevertChangesButton( ref: React.ForwardedRef, ): React.ReactElement { const {selected, ...restProps} = props + const {t} = useTranslation() return ( Date: Mon, 14 Aug 2023 10:20:12 +0200 Subject: [PATCH 051/163] refactor(desk): update error subgroup --- packages/sanity/src/desk/i18n/resources.ts | 12 +++++++----- .../desk/panes/document/timeline/TimelineError.tsx | 4 ++-- .../src/desk/panes/document/timeline/timeline.tsx | 4 ++-- .../desk/panes/document/timeline/timelineMenu.tsx | 4 ++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/sanity/src/desk/i18n/resources.ts b/packages/sanity/src/desk/i18n/resources.ts index 39854a8ec79..8f56bcadf1a 100644 --- a/packages/sanity/src/desk/i18n/resources.ts +++ b/packages/sanity/src/desk/i18n/resources.ts @@ -60,7 +60,7 @@ const deskLocaleStrings = { /** --- Timeline --- */ /** Error prompt when revision cannot be loaded */ - 'timeline.unable-to-load-rev': 'Unable to load revision', + 'timeline.error.unable-to-load-rev': 'Unable to load revision', /** Label for latest version for timeline menu dropdown */ 'timeline.latest-version': 'Latest version', @@ -77,16 +77,18 @@ const deskLocaleStrings = { 'timeline.since-version-missing': 'Since: unknown version', /** Title for error when the timeline for the given document can't be loaded */ - 'timeline.error-title': 'An error occurred whilst retrieving document changes.', + 'timeline.error.load-document-changes-title': + 'An error occurred whilst retrieving document changes.', /** Description for error when the timeline for the given document can't be loaded */ - 'timeline.error-description': 'Document history transactions have not been affected.', + 'timeline.error.load-document-changes-description': + 'Document history transactions have not been affected.', /** Error title for when the document doesn't have history */ - 'timeline.no-document-history-title': 'No document history', + 'timeline.error.no-document-history-title': 'No document history', /** Error description for when the document doesn't have history */ - 'timeline.no-document-history-description': + 'timeline.error.no-document-history-description': 'When changing the content of the document, the document versions will appear in this menu.', /** --- Timeline constants --- */ diff --git a/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx b/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx index 5a4dde48196..85de7c85c4c 100644 --- a/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/TimelineError.tsx @@ -14,10 +14,10 @@ export function TimelineError() { - {t('timeline.error-title')} + {t('timeline.error.load-document-changes-title')} - {t('timeline.error-description')} + {t('timeline.error.load-document-changes-description')} diff --git a/packages/sanity/src/desk/panes/document/timeline/timeline.tsx b/packages/sanity/src/desk/panes/document/timeline/timeline.tsx index 376f334245f..5f8db43deac 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timeline.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timeline.tsx @@ -86,10 +86,10 @@ export const Timeline = ({ {filteredChunks.length === 0 && ( - {t('timeline.no-document-history-title')} + {t('timeline.error.no-document-history-title')} - {t('timeline.no-document-history-description')} + {t('timeline.error.no-document-history-description')} )} diff --git a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx index a0d7ecf16b6..fc8d054bb41 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx @@ -74,7 +74,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { closable: true, description: err.message, status: 'error', - title: t('timeline.unable-to-load-rev'), + title: t('timeline.error.unable-to-load-rev'), }) } }, @@ -92,7 +92,7 @@ export function TimelineMenu({chunk, mode, placement}: TimelineMenuProps) { closable: true, description: err.message, status: 'error', - title: t('timeline.unable-to-load-rev'), + title: t('timeline.error.unable-to-load-rev'), }) } }, From 8c121f2e68ff36a14889d09fab0d950d48c1401a Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 14 Aug 2023 10:36:47 +0200 Subject: [PATCH 052/163] chore(desk): remove unused usememo --- .../sanity/src/desk/panes/document/timeline/timelineMenu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx index fc8d054bb41..56e5f66f566 100644 --- a/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx +++ b/packages/sanity/src/desk/panes/document/timeline/timelineMenu.tsx @@ -1,6 +1,6 @@ import {SelectIcon} from '@sanity/icons' import {Button, Placement, Popover, useClickOutside, useGlobalKeyDown, useToast} from '@sanity/ui' -import React, {useCallback, useMemo, useState} from 'react' +import React, {useCallback, useState} from 'react' import styled from 'styled-components' import {useDocumentPane} from '../useDocumentPane' import {deskLocaleNamespace} from '../../../i18n' From 550e177205906b3f9b67e8c8b421df95781761c9 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 14 Aug 2023 11:02:32 +0200 Subject: [PATCH 053/163] fix(core): add 'action' subgroup to core (change panel) --- .../core/field/diff/components/ChangeList.tsx | 10 +++++---- .../field/diff/components/FieldChange.tsx | 8 ++++--- .../field/diff/components/GroupChange.tsx | 8 ++++--- .../diff/components/RevertChangesButton.tsx | 4 ++-- .../sanity/src/core/i18n/bundles/studio.ts | 22 +++++++++---------- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/packages/sanity/src/core/field/diff/components/ChangeList.tsx b/packages/sanity/src/core/field/diff/components/ChangeList.tsx index de15d405704..50c38e91de2 100644 --- a/packages/sanity/src/core/field/diff/components/ChangeList.tsx +++ b/packages/sanity/src/core/field/diff/components/ChangeList.tsx @@ -118,16 +118,18 @@ export function ChangeList({diff, fields, schemaType}: ChangeListProps): React.R - {t('review-changes.revert-all-button-prompt', {count: changes.length})} + {t('review-changes.action.revert-all-description', { + count: changes.length, + })} diff --git a/packages/sanity/src/core/field/diff/components/GroupChange.tsx b/packages/sanity/src/core/field/diff/components/GroupChange.tsx index 8b5aae11a73..efcd3c08e21 100644 --- a/packages/sanity/src/core/field/diff/components/GroupChange.tsx +++ b/packages/sanity/src/core/field/diff/components/GroupChange.tsx @@ -85,13 +85,15 @@ export function GroupChange( - {t('review-changes.revert-button-prompt')} + {t('review-changes.action.revert-changes-description')} diff --git a/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx b/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx index ee8c6a1e06c..2a2f0354728 100644 --- a/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx +++ b/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx @@ -38,8 +38,8 @@ export const RevertChangesButton = forwardRef(function RevertChangesButton( icon={RevertIcon} selected={selected} // kept at "count: 2" as a const because this component will always have plurals (and never a singular) - // the value itself is not used in translation (check the i18n file "review-changes.revert-button-change_other") - text={t('review-changes.revert-button-change', {count: 2})} + // the value itself is not used in translation (check the i18n file "studio.ts" for the actual translation)") + text={t('review-changes.action.revert-changes-confirm-change', {count: 2})} mode="bleed" padding={1} fontSize={1} diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index 66955f1a254..9c16e1aedca 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -78,28 +78,28 @@ export const studioLocaleStrings = { 'review-changes.no-changes-description': 'Edit the document or select an older version in the timeline to see a list of changes appear in this panel.', - /** Prompt for reverting all changes in document in Review Changes pane */ - 'review-changes.revert-all-button-prompt': `Are you sure you want to revert all {{count}} changes?`, + /** Prompt for reverting all changes in document in Review Changes pane. Includes a count of changes. */ + 'review-changes.action.revert-all-description': `Are you sure you want to revert all {{count}} changes?`, /** Cancel label for revert button prompt action */ - 'review-changes.revert-button-cancel': `Cancel`, + 'review-changes.action.revert-all-cancel': `Cancel`, - /** Revert all label for revert button action - used on prompt button + review changes pane */ - 'review-changes.revert-button-revert-all': `Revert all`, + /** Revert all confirm label for revert button action - used on prompt button + review changes pane */ + 'review-changes.action.revert-all-confirm': `Revert all`, /** Loading author of change in the differences tooltip in the review changes pane */ 'review-changes.loading-author': 'Loading…', /** --- Review Changes: Field + Group --- */ - /** Revert changes prompt for field change */ - 'review-changes.revert-button-prompt': `Are you sure you want to revert the changes?`, + /** Prompt for reverting changes for a field change */ + 'review-changes.action.revert-changes-description': `Are you sure you want to revert the changes?`, - /** Revert change (singular) label for field change action */ - 'review-changes.revert-button-change_one': `Revert change`, + /** Prompt for confirming revert change (singular) label for field change action */ + 'review-changes.action.revert-changes-confirm-change_one': `Revert change`, - /** Revert changes (plural) label for field change action */ - 'review-changes.revert-button-change_other': `Revert changes`, + /** Revert for confirming revert (plural) label for field change action */ + 'review-changes.action.revert-changes-confirm-change_other': `Revert changes`, } /** From 982c6990c0d1644c76950ff1e548c1f57290feeb Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 14 Aug 2023 11:06:03 +0200 Subject: [PATCH 054/163] fix(core): add 'action' subgroup to desk (change panel) --- packages/sanity/src/desk/i18n/resources.ts | 2 +- .../desk/panes/document/inspectors/changes/ChangesInspector.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/desk/i18n/resources.ts b/packages/sanity/src/desk/i18n/resources.ts index 8f56bcadf1a..c54fb8530c1 100644 --- a/packages/sanity/src/desk/i18n/resources.ts +++ b/packages/sanity/src/desk/i18n/resources.ts @@ -49,7 +49,7 @@ const deskLocaleStrings = { 'review-changes.title': 'Review changes', /** Label for the close button label in Review Changes pane */ - 'review-changes.close-label': 'Close review changes', + 'review-changes.action.close-label': 'Close review changes', /** Label and text for differences tooltip that indicates the authors of the changes */ 'review-changes.changes-by-author': 'Changes by', diff --git a/packages/sanity/src/desk/panes/document/inspectors/changes/ChangesInspector.tsx b/packages/sanity/src/desk/panes/document/inspectors/changes/ChangesInspector.tsx index e533190cef5..fb4b1feff83 100644 --- a/packages/sanity/src/desk/panes/document/inspectors/changes/ChangesInspector.tsx +++ b/packages/sanity/src/desk/panes/document/inspectors/changes/ChangesInspector.tsx @@ -66,7 +66,7 @@ export function ChangesInspector(props: DocumentInspectorProps): ReactElement { Date: Wed, 30 Aug 2023 08:23:22 -0700 Subject: [PATCH 055/163] refactor: move all change-related resources to core --- .../__workshop__/RevertChangesButtonStory.tsx | 2 +- .../core/field/diff/components/ChangeList.tsx | 8 ++--- .../field/diff/components/DiffTooltip.tsx | 2 +- .../field/diff/components/FieldChange.tsx | 7 +++-- .../field/diff/components/GroupChange.tsx | 9 +++--- .../core/field/diff/components/NoChanges.tsx | 4 +-- .../diff/components/RevertChangesButton.tsx | 8 ++--- .../sanity/src/core/i18n/bundles/studio.ts | 30 +++++++++++++------ packages/sanity/src/desk/i18n/resources.ts | 14 --------- .../inspectors/changes/ChangesInspector.tsx | 13 ++++---- .../inspectors/changes/LoadingContent.tsx | 5 ++-- 11 files changed, 50 insertions(+), 52 deletions(-) diff --git a/packages/sanity/src/core/field/__workshop__/RevertChangesButtonStory.tsx b/packages/sanity/src/core/field/__workshop__/RevertChangesButtonStory.tsx index a8afa5b3656..651668abcd2 100644 --- a/packages/sanity/src/core/field/__workshop__/RevertChangesButtonStory.tsx +++ b/packages/sanity/src/core/field/__workshop__/RevertChangesButtonStory.tsx @@ -5,7 +5,7 @@ import {RevertChangesButton} from '../diff/components/RevertChangesButton' export default function RevertChangesButtonStory() { return ( - + ) } diff --git a/packages/sanity/src/core/field/diff/components/ChangeList.tsx b/packages/sanity/src/core/field/diff/components/ChangeList.tsx index 50c38e91de2..cacc649bb20 100644 --- a/packages/sanity/src/core/field/diff/components/ChangeList.tsx +++ b/packages/sanity/src/core/field/diff/components/ChangeList.tsx @@ -118,18 +118,18 @@ export function ChangeList({diff, fields, schemaType}: ChangeListProps): React.R - {t('review-changes.action.revert-all-description', { + {t('changes.action.revert-all-description', { count: changes.length, })} @@ -129,6 +129,7 @@ export function FieldChange( > - {t('review-changes.action.revert-changes-description')} + {t('changes.action.revert-changes-description')} @@ -100,12 +100,13 @@ export function GroupChange( } portal padding={4} - placement={'left'} + placement="left" open={confirmRevertOpen} ref={setRevertPopoverElement} > - {t('review-changes.no-changes-title')} + {t('changes.no-changes-title')} - {t('review-changes.no-changes-description')} + {t('changes.no-changes-description')} ) diff --git a/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx b/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx index 2a2f0354728..055aa9ac09d 100644 --- a/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx +++ b/packages/sanity/src/core/field/diff/components/RevertChangesButton.tsx @@ -27,19 +27,17 @@ const Root = styled(Button)` /** @internal */ export const RevertChangesButton = forwardRef(function RevertChangesButton( - props: ButtonProps & Omit, 'ref'>, + props: ButtonProps & Omit, 'ref'> & {changeCount: number}, ref: React.ForwardedRef, ): React.ReactElement { - const {selected, ...restProps} = props + const {selected, changeCount, ...restProps} = props const {t} = useTranslation() return ( ({ @@ -66,10 +67,10 @@ export function ChangesInspector(props: DocumentInspectorProps): ReactElement { @@ -79,10 +80,10 @@ export function ChangesInspector(props: DocumentInspectorProps): ReactElement { - + {changeAnnotations.map(({author}) => ( ))} diff --git a/packages/sanity/src/desk/panes/document/inspectors/changes/LoadingContent.tsx b/packages/sanity/src/desk/panes/document/inspectors/changes/LoadingContent.tsx index fb01c2d1da3..a90e0d79d5a 100644 --- a/packages/sanity/src/desk/panes/document/inspectors/changes/LoadingContent.tsx +++ b/packages/sanity/src/desk/panes/document/inspectors/changes/LoadingContent.tsx @@ -1,11 +1,10 @@ import React from 'react' import {Box, Flex, Spinner, Text} from '@sanity/ui' import {Delay} from '../../../../components' -import {deskLocaleNamespace} from '../../../../i18n' import {useTranslation} from 'sanity' export function LoadingContent() { - const {t} = useTranslation(deskLocaleNamespace) + const {t} = useTranslation('studio') return ( @@ -13,7 +12,7 @@ export function LoadingContent() { - {t('review-changes.loading-changes')} + {t('changes.loading-changes')} From a4cddf7976348c303f232334baae1670928fc73d Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Wed, 30 Aug 2023 08:38:28 -0700 Subject: [PATCH 056/163] refactor: pluralize revert changes confirmation --- packages/sanity/src/core/field/diff/components/FieldChange.tsx | 2 +- packages/sanity/src/core/field/diff/components/GroupChange.tsx | 2 +- packages/sanity/src/core/i18n/bundles/studio.ts | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/core/field/diff/components/FieldChange.tsx b/packages/sanity/src/core/field/diff/components/FieldChange.tsx index be5efd7a9ff..4ff6245da0c 100644 --- a/packages/sanity/src/core/field/diff/components/FieldChange.tsx +++ b/packages/sanity/src/core/field/diff/components/FieldChange.tsx @@ -109,7 +109,7 @@ export function FieldChange( - {t('changes.action.revert-changes-description')} + {t('changes.action.revert-changes-description', {count: 1})} From b41fe80edc137a75de376d655e87e85f27dad571 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Thu, 17 Aug 2023 15:12:03 +0200 Subject: [PATCH 086/163] feat(core): add i18n primitives to Instructions --- packages/sanity/src/core/i18n/bundles/studio.ts | 3 +++ .../components/navbar/search/components/Instructions.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index bce68f794e6..e362e6ce817 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -259,6 +259,9 @@ export const studioLocaleStrings = { /** Label for action to clear recent searches */ 'navbar.search.action.clear-recent-searches': 'Clear recent searches', + + /** Label for instructions on how to use the search when no recent searches are available */ + 'navbar.search.instructions': 'Use the following icon to refine your search', } /** diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/Instructions.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/Instructions.tsx index 4fd4f53cc20..63cb2777f00 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/Instructions.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/Instructions.tsx @@ -1,16 +1,18 @@ import {ControlsIcon} from '@sanity/icons' import {Flex, Inline, Text} from '@sanity/ui' import React from 'react' +import {useTranslation} from '../../../../../i18n' export function Instructions() { + const {t} = useTranslation() + return ( - Use + {t('navbar.search.instructions')} - to refine your search ) From c56beacd18e4716a5b76ddb43be1d0b6cca880d0 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Thu, 17 Aug 2023 15:52:32 +0200 Subject: [PATCH 087/163] feat(core): add i18n primitives to ConfigIssuesButton --- packages/sanity/src/core/i18n/bundles/studio.ts | 15 +++++++++++++++ .../navbar/configIssues/ConfigIssuesButton.tsx | 15 ++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index e362e6ce817..9c439dfa02b 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -262,6 +262,21 @@ export const studioLocaleStrings = { /** Label for instructions on how to use the search when no recent searches are available */ 'navbar.search.instructions': 'Use the following icon to refine your search', + + /** --- Configuration Issues --- */ + + /** label for when when there are configuration issues */ + 'navbar.configuration.error.found-configuration-issues-status': 'Found configuration issues', + + /** Error title for when there are configuration issues */ + 'navbar.configuration.error.configuration-issues-title': 'Configuration issues', + + /** Error description for when there are configuration issues, explaining that the checks are only performed during development */ + 'navbar.configuration.error.configuration-issues-description': + 'Configuration checks are only performed during development and will not be visible in production builds', + + /** Warning label that tells the user how many warnings were found */ + 'navbar.configuration.found-number-schema-warning': `Found {{count}} schema warnings`, } /** diff --git a/packages/sanity/src/core/studio/components/navbar/configIssues/ConfigIssuesButton.tsx b/packages/sanity/src/core/studio/components/navbar/configIssues/ConfigIssuesButton.tsx index d978d856532..d31beacde0b 100644 --- a/packages/sanity/src/core/studio/components/navbar/configIssues/ConfigIssuesButton.tsx +++ b/packages/sanity/src/core/studio/components/navbar/configIssues/ConfigIssuesButton.tsx @@ -5,6 +5,7 @@ import {useSchema} from '../../../../hooks' import {SchemaProblemGroups} from '../../../screens/schemaErrors/SchemaProblemGroups' import {useColorScheme} from '../../../colorScheme' import {StatusButton} from '../../../../components' +import {useTranslation} from '../../../../i18n' export function ConfigIssuesButton() { const schema = useSchema() @@ -15,6 +16,7 @@ export function ConfigIssuesButton() { // get root scheme const {scheme} = useColorScheme() + const {t} = useTranslation() const dialogId = useId() @@ -38,7 +40,7 @@ export function ConfigIssuesButton() { <> - Configuration issues + + {t('navbar.configuration.error.configuration-issues-title')} + - Configuration checks are only performed during development and will not be visible - in production builds + {t('navbar.configuration.error.configuration-issues-description')} } @@ -67,7 +70,9 @@ export function ConfigIssuesButton() { > - Found {groupsWithWarnings.length} schema warnings + {t('navbar.configuration.found-number-schema-warning', { + count: groupsWithWarnings.length, + })} From 67d182718f4264cf3985da4144c6e83597880e44 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Thu, 17 Aug 2023 15:58:22 +0200 Subject: [PATCH 088/163] feat(core): add i18n primitives to SchemaProblemGroups --- packages/sanity/src/core/i18n/bundles/studio.ts | 6 ++++++ .../studio/screens/schemaErrors/SchemaProblemGroups.tsx | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index 9c439dfa02b..e930c7d5a1e 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -277,6 +277,12 @@ export const studioLocaleStrings = { /** Warning label that tells the user how many warnings were found */ 'navbar.configuration.found-number-schema-warning': `Found {{count}} schema warnings`, + + /** Label for displaying the schema error and warnings for the studio configurations */ + 'navbar.configuration.type-label': 'type', + + /** Prompt to view documentation about the schema problems */ + 'navbar.configuration.action.view-documentation': 'View documentation', } /** diff --git a/packages/sanity/src/core/studio/screens/schemaErrors/SchemaProblemGroups.tsx b/packages/sanity/src/core/studio/screens/schemaErrors/SchemaProblemGroups.tsx index c9ef3fef9e7..689fda695ba 100644 --- a/packages/sanity/src/core/studio/screens/schemaErrors/SchemaProblemGroups.tsx +++ b/packages/sanity/src/core/studio/screens/schemaErrors/SchemaProblemGroups.tsx @@ -5,6 +5,7 @@ import {SchemaValidationProblemGroup} from '@sanity/types' import React, {useMemo} from 'react' import styled from 'styled-components' import {capitalize} from 'lodash' +import {useTranslation} from '../../../i18n' const TONES: Record<'error' | 'warning', ThemeColorToneKey> = { error: 'critical', @@ -24,6 +25,7 @@ const ErrorMessageText = styled(Text)` export function SchemaProblemGroups(props: {problemGroups: SchemaValidationProblemGroup[]}) { const {problemGroups} = props + const {t} = useTranslation() const items = useMemo(() => { const ret = [] @@ -57,7 +59,8 @@ export function SchemaProblemGroups(props: {problemGroups: SchemaValidationProbl {schemaType ? ( <> - {capitalize(schemaType.type)} type "{schemaType.name}" + {capitalize(schemaType.type)} {t('navbar.configuration.type-label')} " + {schemaType.name}" ) : null} @@ -111,7 +114,7 @@ export function SchemaProblemGroups(props: {problemGroups: SchemaValidationProbl target="_blank" rel="noopener noreferrer" > - View documentation → + {t('navbar.configuration.action.view-documentation')} → )} From c12244e9e723355ea2666194da66df8b0d2399e9 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Thu, 17 Aug 2023 16:34:20 +0200 Subject: [PATCH 089/163] feat(core): add i18n primitives to ResourcesButton --- packages/sanity/src/core/i18n/bundles/studio.ts | 5 +++++ .../studio/components/navbar/resources/ResourcesButton.tsx | 6 ++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index e930c7d5a1e..44e73be38c9 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -283,6 +283,11 @@ export const studioLocaleStrings = { /** Prompt to view documentation about the schema problems */ 'navbar.configuration.action.view-documentation': 'View documentation', + + /** --- Help & Resources Menu --- */ + + /** Title for help and resources menus */ + 'navbar.helpResources.title': 'Help and resources', } /** diff --git a/packages/sanity/src/core/studio/components/navbar/resources/ResourcesButton.tsx b/packages/sanity/src/core/studio/components/navbar/resources/ResourcesButton.tsx index c017fbc12fb..4e8033a4b12 100644 --- a/packages/sanity/src/core/studio/components/navbar/resources/ResourcesButton.tsx +++ b/packages/sanity/src/core/studio/components/navbar/resources/ResourcesButton.tsx @@ -3,6 +3,7 @@ import {Box, Button, Flex, Menu, MenuButton, Text, Tooltip} from '@sanity/ui' import React, {useCallback, useState} from 'react' import styled from 'styled-components' import {useColorScheme} from '../../../colorScheme' +import {useTranslation} from '../../../../i18n' import {useGetHelpResources} from './helper-functions/hooks' import {ResourcesMenuItems} from './ResourcesMenuItems' @@ -14,6 +15,7 @@ const StyledMenu = styled(Menu)` export function ResourcesButton() { const {scheme} = useColorScheme() const [menuOpen, setMenuOpen] = useState(false) + const {t} = useTranslation() const {value, error, isLoading} = useGetHelpResources() @@ -25,7 +27,7 @@ export function ResourcesButton() { - Help and resources + {t('navbar.helpResources.title')} } scheme={scheme} @@ -37,7 +39,7 @@ export function ResourcesButton() { Date: Thu, 17 Aug 2023 16:35:13 +0200 Subject: [PATCH 090/163] feat(core): add i18n primitives to ResourcesMenuItems --- .../sanity/src/core/i18n/bundles/studio.ts | 15 ++++ .../navbar/resources/ResourcesMenuItems.tsx | 68 ++++++++++--------- 2 files changed, 51 insertions(+), 32 deletions(-) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index 44e73be38c9..1ae736b984a 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -288,6 +288,21 @@ export const studioLocaleStrings = { /** Title for help and resources menus */ 'navbar.helpResources.title': 'Help and resources', + + /** Information for what studio version the current studio is running */ + 'navbar.helpResources.studio-version': `Sanity Studio version {{studioVersion}}`, + + /** Information for what the latest sanity version is */ + 'navbar.helpResources.latest-sanity-version': `Latest version is {{latestVersion}}`, + + /** Label for "join our community" call to action */ + 'navbar.helpResources.action.join-our-community': `Join our community`, + + /** Label for "help and support" call to action */ + 'navbar.helpResources.action.help-and-support': `Help and support`, + + /** Label for "contact sales" call to action */ + 'navbar.helpResources.action.contact-sales': `Contact sales`, } /** diff --git a/packages/sanity/src/core/studio/components/navbar/resources/ResourcesMenuItems.tsx b/packages/sanity/src/core/studio/components/navbar/resources/ResourcesMenuItems.tsx index a106b0fec1e..c01675f90f0 100644 --- a/packages/sanity/src/core/studio/components/navbar/resources/ResourcesMenuItems.tsx +++ b/packages/sanity/src/core/studio/components/navbar/resources/ResourcesMenuItems.tsx @@ -1,6 +1,7 @@ import {Box, Card, Flex, Label, MenuDivider, MenuItem, Spinner, Text} from '@sanity/ui' import React from 'react' import {SANITY_VERSION} from '../../../../version' +import {useTranslation} from '../../../../i18n' import {ResourcesResponse, Section} from './helper-functions/types' interface ResourcesMenuItemProps { @@ -12,6 +13,7 @@ interface ResourcesMenuItemProps { export function ResourcesMenuItems({error, isLoading, value}: ResourcesMenuItemProps) { const sections = value?.resources?.sectionArray const latestStudioVersion = value?.latestVersion + const {t} = useTranslation() if (isLoading) { return ( @@ -21,6 +23,36 @@ export function ResourcesMenuItems({error, isLoading, value}: ResourcesMenuItemP ) } + const fallbackLinks = ( + <> + + + + + + ) + return ( <> {/* Display fallback values on error / no response */} @@ -35,12 +67,14 @@ export function ResourcesMenuItems({error, isLoading, value}: ResourcesMenuItemP {/* Studio version information */} - Sanity Studio version {SANITY_VERSION} + {t('navbar.helpResources.studio-version', {studioVersion: SANITY_VERSION})} {!error && latestStudioVersion && ( - Latest version is {latestStudioVersion} + {t('navbar.helpResources.latest-sanity-version', { + latestVersion: latestStudioVersion, + })} )} @@ -49,36 +83,6 @@ export function ResourcesMenuItems({error, isLoading, value}: ResourcesMenuItemP ) } -const fallbackLinks = ( - <> - - - - - -) - function SubSection({subSection}: {subSection: Section}) { return ( <> From a505d3dd44439749647a614be5f48be9611836f2 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 21 Aug 2023 11:22:46 +0200 Subject: [PATCH 091/163] feat(core): add i18n primitives to UserMenu + AppearanceMenu --- .../sanity/src/core/i18n/bundles/studio.ts | 35 +++++++++++++++++++ .../sanity/src/core/studio/colorScheme.tsx | 20 ++++++----- .../components/navbar/userMenu/UserMenu.tsx | 20 ++++++----- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index 1ae736b984a..53747c859fe 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -303,6 +303,41 @@ export const studioLocaleStrings = { /** Label for "contact sales" call to action */ 'navbar.helpResources.action.contact-sales': `Contact sales`, + + /** --- User Menu --- */ + + /** Label for tooltip to show which provider the currently logged in user is using */ + 'navbar.user-menu.login-provider': `Signed in with {{providerTitle}}`, + + /** Label for action to manage the current sanity project, used for accessibility as well */ + 'navbar.user-menu.action.manage-project': 'Manage project', + + /** Label for action to invite members to the current sanity project, used for accessibility as well */ + 'navbar.user-menu.action.invite-members': 'Invite members', + + /** Label for action to sign out of the current sanity project */ + 'navbar.user-menu.action.sign-out': 'Sign out', + + /** Title for appearance section for the current studio (dark / light scheme) */ + 'navbar.user-menu.appearance-title': 'Appearance', + + /** Title for using system apparence in the apperance user menu */ + 'navbar.user-menu.color-scheme.system-title': 'System', + + /** Description for using "system apparence" in the apperance user menu */ + 'navbar.user-menu.color-scheme.system-description': 'Use system appearance', + + /** Title for using the "dark theme" in the apperance user menu */ + 'navbar.user-menu.color-scheme.dark-title': 'Dark', + + /** Description for using the "dark theme" in the apperance user menu */ + 'navbar.user-menu.color-scheme.dark-description': 'Use dark appearance', + + /** Title for using the "light theme" in the apperance user menu */ + 'navbar.user-menu.color-scheme.light-title': 'Light', + + /** Description for using the "light theme" in the apperance user menu */ + 'navbar.user-menu.color-scheme.light-description': 'Use light appearance', } /** diff --git a/packages/sanity/src/core/studio/colorScheme.tsx b/packages/sanity/src/core/studio/colorScheme.tsx index b1052251f2c..dcea958feed 100644 --- a/packages/sanity/src/core/studio/colorScheme.tsx +++ b/packages/sanity/src/core/studio/colorScheme.tsx @@ -2,6 +2,7 @@ import React, {createContext, useContext, useEffect, useMemo, useSyncExternalSto import {studioTheme, ThemeColorSchemeKey, ThemeProvider, usePrefersDark} from '@sanity/ui' import {DesktopIcon, MoonIcon, SunIcon} from '@sanity/icons' import type {StudioThemeColorSchemeKey} from '../theme/types' +import {TFunction} from '../i18n' /** * Used to keep track of the internal value, which can be "system" in addition to "light" and "dark" @@ -201,35 +202,38 @@ interface ColorSchemeOption { /** * @internal */ -export function useColorSchemeOptions(setScheme: (nextScheme: StudioThemeColorSchemeKey) => void) { +export function useColorSchemeOptions( + setScheme: (nextScheme: StudioThemeColorSchemeKey) => void, + t: TFunction<'', undefined> +) { const scheme = _useColorSchemeInternalValue() return useMemo(() => { return [ { - title: 'System', + title: t('navbar.user-menu.color-scheme.system-title'), name: 'system', - label: 'Use system appearance', + label: t('navbar.user-menu.color-scheme.system-description'), selected: scheme === 'system', onSelect: () => setScheme('system'), icon: DesktopIcon, }, { - title: 'Dark', + title: t('navbar.user-menu.color-scheme.dark-title'), name: 'dark', - label: 'Use dark appearance', + label: t('navbar.user-menu.color-scheme.dark-description'), selected: scheme === 'dark', onSelect: () => setScheme('dark'), icon: MoonIcon, }, { - title: 'Light', + title: t('navbar.user-menu.color-scheme.light-title'), name: 'light', - label: 'Use light appearance', + label: t('navbar.user-menu.color-scheme.light-description'), selected: scheme === 'light', onSelect: () => setScheme('light'), icon: SunIcon, }, ] satisfies ColorSchemeOption[] - }, [scheme, setScheme]) + }, [scheme, setScheme, t]) } diff --git a/packages/sanity/src/core/studio/components/navbar/userMenu/UserMenu.tsx b/packages/sanity/src/core/studio/components/navbar/userMenu/UserMenu.tsx index a8aba895d5b..c144a7e303e 100644 --- a/packages/sanity/src/core/studio/components/navbar/userMenu/UserMenu.tsx +++ b/packages/sanity/src/core/studio/components/navbar/userMenu/UserMenu.tsx @@ -26,6 +26,7 @@ import { } from '../../../colorScheme' import {useWorkspace} from '../../../workspace' import {userHasRole} from '../../../../util/userHasRole' +import {useTranslation} from '../../../../i18n' import {LoginProviderLogo} from './LoginProviderLogo' import {LocaleMenu} from './LocaleMenu' @@ -43,8 +44,9 @@ const AvatarBox = styled(Box)` ` function AppearanceMenu({setScheme}: {setScheme: (nextScheme: StudioThemeColorSchemeKey) => void}) { + const {t} = useTranslation() // Subscribe to just what we need, if the menu isn't shown then we're not subscribed to these contexts - const options = useColorSchemeOptions(setScheme) + const options = useColorSchemeOptions(setScheme, t) return ( <> @@ -52,7 +54,7 @@ function AppearanceMenu({setScheme}: {setScheme: (nextScheme: StudioThemeColorSc @@ -79,6 +81,8 @@ export function UserMenu() { const isAdmin = Boolean(currentUser && userHasRole(currentUser, 'administrator')) const providerTitle = getProviderTitle(currentUser?.provider) + const {t} = useTranslation() + const popoverProps: MenuButtonProps['popover'] = useMemo( () => ({ placement: 'bottom-end', @@ -113,7 +117,7 @@ export function UserMenu() { content={ providerTitle && ( - Signed in with {providerTitle} + {t('navbar.user-menu.login-provider', {providerTitle})} ) } @@ -143,19 +147,19 @@ export function UserMenu() { {isAdmin && ( )} @@ -165,7 +169,7 @@ export function UserMenu() { From e36b46b04987854fbd32251b4554571789f08674 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 21 Aug 2023 11:24:43 +0200 Subject: [PATCH 092/163] feat(core): add i18n primitives to LocaleMenu --- packages/sanity/src/core/i18n/bundles/studio.ts | 3 +++ .../core/studio/components/navbar/userMenu/LocaleMenu.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index 53747c859fe..8833af9cf11 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -338,6 +338,9 @@ export const studioLocaleStrings = { /** Description for using the "light theme" in the apperance user menu */ 'navbar.user-menu.color-scheme.light-description': 'Use light appearance', + + /** Title for locale section for the current studio */ + 'navbar.user-menu.locale-title': 'Language', } /** diff --git a/packages/sanity/src/core/studio/components/navbar/userMenu/LocaleMenu.tsx b/packages/sanity/src/core/studio/components/navbar/userMenu/LocaleMenu.tsx index 82042358d84..d463281a3ae 100644 --- a/packages/sanity/src/core/studio/components/navbar/userMenu/LocaleMenu.tsx +++ b/packages/sanity/src/core/studio/components/navbar/userMenu/LocaleMenu.tsx @@ -1,9 +1,11 @@ import {Box, Label, MenuDivider, MenuItem} from '@sanity/ui' import React, {useCallback} from 'react' -import {useLocale} from '../../../../i18n' +import {useLocale, useTranslation} from '../../../../i18n' export function LocaleMenu() { const {changeLocale, currentLocale, locales} = useLocale() + const {t} = useTranslation() + if (!locales || locales.length < 2) { return null } @@ -14,7 +16,7 @@ export function LocaleMenu() { From 21aaf9e1589915c6d14ffdab6c3b4e63500ea1b9 Mon Sep 17 00:00:00 2001 From: RitaDias Date: Mon, 21 Aug 2023 11:29:54 +0200 Subject: [PATCH 093/163] feat(core): add i18n primitives to NavDrawer --- .../core/studio/components/navbar/NavDrawer.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/sanity/src/core/studio/components/navbar/NavDrawer.tsx b/packages/sanity/src/core/studio/components/navbar/NavDrawer.tsx index b72e1e7e145..b8cc0be4805 100644 --- a/packages/sanity/src/core/studio/components/navbar/NavDrawer.tsx +++ b/packages/sanity/src/core/studio/components/navbar/NavDrawer.tsx @@ -20,6 +20,7 @@ import {useWorkspaces} from '../../workspaces' import {useColorSchemeOptions, useColorSchemeSetValue} from '../../colorScheme' import {StudioThemeColorSchemeKey} from '../../../theme' import {userHasRole} from '../../../util/userHasRole' +import {useTranslation} from '../../../i18n' import {WorkspaceMenuButton} from './workspace' const ANIMATION_TRANSITION: Transition = { @@ -72,15 +73,16 @@ const InnerCard = styled(motion(Card))` ` function AppearanceMenu({setScheme}: {setScheme: (nextScheme: StudioThemeColorSchemeKey) => void}) { + const {t} = useTranslation() // Subscribe to just what we need, if the menu isn't shown then we're not subscribed to these contexts - const options = useColorSchemeOptions(setScheme) + const options = useColorSchemeOptions(setScheme, t) return ( <> @@ -121,6 +123,7 @@ export const NavDrawer = memo(function NavDrawer(props: NavDrawerProps) { const ToolMenu = useToolMenuComponent() const isAdmin = Boolean(currentUser && userHasRole(currentUser, 'administrator')) + const {t} = useTranslation() const handleKeyDown = useCallback( (event: React.KeyboardEvent) => { @@ -205,28 +208,28 @@ export const NavDrawer = memo(function NavDrawer(props: NavDrawerProps) { diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/SearchResults.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/SearchResults.tsx index 481cecec8ae..265eb6af11f 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/SearchResults.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/searchResults/SearchResults.tsx @@ -89,7 +89,7 @@ export function SearchResults({inputElement}: SearchResultsProps) { {hasSearchResults && ( -}): string { +}): {types: string[]; remainingCount: number} { + if (types.length === 0) { + return {remainingCount: 0, types: []} + } + /** * Get the total number of visible document types whose titles fit within `availableCharacters` count. * The first document is always included, regardless of whether it fits within `availableCharacters` or not. @@ -35,16 +49,49 @@ export function documentTypesTruncated({ [], ) - const remainingCount = types.length - visibleTypes.length + return { + remainingCount: types.length - visibleTypes.length, + types: visibleTypes.map(typeTitle), + } +} - if (!types.length) { - return t('navbar.search.all-types-label') +/** + * From the list of provided document types, return as many type names as possible that can fit + * within the `availableCharacters` parameter, formatted by title where available. Includes the + * number of remaining types that were _not_ included in the returned array of types, so a UI can + * choose to display a "+x more" suffix. + * + * @param options - Options object + * @returns A formatted string of types + * @internal + */ +export function documentTypesTruncated({ + t, + availableCharacters, + types, +}: { + availableCharacters?: number + types: SearchableType[] + t: TFunction<'studio', undefined> +}): string { + if (types.length === 0) { + return t('search.document-type-list-all-types') } - return [ - `${visibleTypes.map(typeTitle).join(', ')}`, - ...(remainingCount ? [`${t('navbar.search.remaining-document-types', {remainingCount})}`] : []), - ].join(' ') + const {remainingCount, types: visibleTypes} = getDocumentTypesTruncated({ + availableCharacters, + types, + }) + + const key = + remainingCount > 0 ? 'search.document-type-list-truncated' : 'search.document-type-list' + + // "Author, Book" or "Author, Book, Pet, Person +2 more" + return t(key, { + count: remainingCount, + types: visibleTypes, + formatParams: {types: {style: 'short', type: 'unit'}}, + }) } function typeTitle(schemaType: SearchableType) { From 0188a665df9668a390fc1da3a2dd621bbeebcdee Mon Sep 17 00:00:00 2001 From: Espen Hovlandsdal Date: Tue, 12 Sep 2023 17:01:55 -0700 Subject: [PATCH 128/163] refactor(i18n): align remaining search filter resources --- .../sanity/src/core/i18n/bundles/studio.ts | 20 +++++++++---------- .../components/filters/common/ButtonValue.tsx | 2 +- .../filters/filter/inputs/asset/Asset.tsx | 2 +- .../filters/filter/inputs/boolean/Boolean.tsx | 4 ++-- .../filters/filter/inputs/number/Number.tsx | 2 +- .../filter/inputs/number/NumberRange.tsx | 4 ++-- .../filter/inputs/reference/Reference.tsx | 2 +- .../filters/filter/inputs/string/String.tsx | 2 +- .../filter/inputs/string/StringList.tsx | 2 +- 9 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/sanity/src/core/i18n/bundles/studio.ts b/packages/sanity/src/core/i18n/bundles/studio.ts index 05f967cf0ff..0d14fc85463 100644 --- a/packages/sanity/src/core/i18n/bundles/studio.ts +++ b/packages/sanity/src/core/i18n/bundles/studio.ts @@ -319,35 +319,35 @@ export const studioLocaleStrings = { 'search.filter-shared-fields-header': 'Shared fields', /** Label for boolean filter - true */ - 'search.filter.true': 'True', + 'search.filter-boolean-true': 'True', /** Label for boolean filter - false */ - 'search.filter.false': 'False', + 'search.filter-boolean-false': 'False', /** Placeholder value for the string filter */ - 'search.filter.string-value-placeholder': 'Value', + 'search.filter-string-value-placeholder': 'Value', /** Placeholder value for the number filter */ - 'search.filter.number-value-placeholder': 'Value', + 'search.filter-number-value-placeholder': 'Value', /** Placeholder value for minimum numeric value filter */ - 'search.filter.number-min-value-placeholder': 'Min value', + 'search.filter-number-min-value-placeholder': 'Min value', /** Placeholder value for maximum numeric value filter */ - 'search.filter.number-max-value-placeholder': 'Max value', + 'search.filter-number-max-value-placeholder': 'Max value', /** Label/placeholder prompting user to select one of the predefined, allowed values for a string field */ - 'search.filter.string-value-select-predefined-value': 'Select...', + 'search.filter-string-value-select-predefined-value': 'Select...', /** Label for the action of clearing the currently selected asset in an image/file filter */ - 'search.filter.asset-clear': 'Clear', + 'search.filter-asset-clear': 'Clear', /** Label for the action of clearing the currently selected document in a reference filter */ - 'search.filter.reference-clear': 'Clear', + 'search.filter-reference-clear': 'Clear', /** Label for search value in a range of numbers */ // @todo Part of `arrayOperators` - needs `` refactoring - 'search.filter.number-items-range': `{{min}} → {{max}} items`, + 'search.filter-number-items-range': `{{min}} → {{max}} items`, /** Title label for when no search results are found */ 'search.no-results-title': 'No results found', diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/common/ButtonValue.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/common/ButtonValue.tsx index 3079d872caa..72dc2a47b86 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/common/ButtonValue.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/common/ButtonValue.tsx @@ -18,7 +18,7 @@ const DEFAULT_DATE_FORMAT = 'yyyy-MM-dd' export function SearchButtonValueBoolean({value}: OperatorButtonValueComponentProps) { const {t} = useTranslation() - return <>{value ? t('search.filter.true') : t('search.filter.false')} + return <>{value ? t('search.filter-boolean-true') : t('search.filter-boolean-false')} } export function SearchButtonValueDate({ diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/asset/Asset.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/asset/Asset.tsx index bda4a839617..3be1304decd 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/asset/Asset.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/asset/Asset.tsx @@ -188,7 +188,7 @@ export function SearchFilterAssetInput(type?: AssetType) { mode="ghost" onClick={handleClear} style={{flex: 1}} - text={t('search.filter.asset-clear')} + text={t('search.filter-asset-clear')} tone="critical" /> )} diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/boolean/Boolean.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/boolean/Boolean.tsx index a0d85e9a7e2..196e827c4ee 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/boolean/Boolean.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/boolean/Boolean.tsx @@ -24,8 +24,8 @@ export function SearchFilterBooleanInput({onChange, value}: OperatorInputCompone radius={2} value={String(value ?? true)} > - - + + ) } diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/number/Number.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/number/Number.tsx index 0b50825ec21..91a167846b4 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/number/Number.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/number/Number.tsx @@ -25,7 +25,7 @@ export function SearchFilterNumberInput({value, onChange}: OperatorInputComponen diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/string/String.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/string/String.tsx index 1dc822d00a5..8b76d9bb7a6 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/string/String.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/string/String.tsx @@ -22,7 +22,7 @@ export function SearchFilterStringInput({ diff --git a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/string/StringList.tsx b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/string/StringList.tsx index 727338ad483..d40305c413a 100644 --- a/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/string/StringList.tsx +++ b/packages/sanity/src/core/studio/components/navbar/search/components/filters/filter/inputs/string/StringList.tsx @@ -113,7 +113,7 @@ export function SearchFilterStringListInput({