Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(i18n): convert reference input strings to use i18n primitives #4977

Merged
merged 8 commits into from
Oct 11, 2023
94 changes: 91 additions & 3 deletions dev/test-studio/plugins/locale-no-nb/bundles/studio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ const studioResources: Record<StudioLocaleResourceKeys, string> = {
'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',
'timeAgo.seconds.ago_other': '{{count}} sekunder siden',
/* Relative time, granularity: seconds, using a minimal format*/
'timeAgo.seconds.minimal': '{{count}}m',
'timeAgo.seconds.minimal': '{{count}}s',
/* Relative time, granularity: seconds, using a minimal format, configured to show ago suffix*/
'timeAgo.seconds.minimal.ago': '{{count}}m ago',
'timeAgo.seconds.minimal.ago': '{{count}}s siden',

/** --- DateTime (and Date) Input --- */

Expand Down Expand Up @@ -101,6 +101,94 @@ const studioResources: Record<StudioLocaleResourceKeys, string> = {

/** Label for selecting a hour preset. Receives a `time` param as a string on hh:mm format and a `date` param as a Date instance denoting the preset date */
'inputs.datetime.calendar.action.set-to-time-preset': '{{time}} on {{date, datetime}}',

/** --- Reference (and Cross-Dataset Reference) Input --- */

/** Error title for when the search for a reference failed. Note that the message sent by the backend may not be localized. */
'inputs.reference.error.search-failed-title': `Referansesøk mislyktes`,

/** Error title for when the current reference value points to a document that does not exist (on weak references) */
'inputs.reference.error.nonexistent-document-title': 'Ikke funnet',

/** Error description for when the current reference value points to a document that does not exist (on weak references) */
'inputs.reference.error.nonexistent-document-description': `Det refererte dokumentet eksisterer ikke (ID: <Code>{{documentId}}</Code>). Du kan enten fjerne referansen eller erstatte den med et annet dokument.`,

/** Error title for when the referenced document failed to be loaded */
'inputs.reference.error.failed-to-load-document-title': 'Kunne ikke laste referert dokument',

/** Error title for when the user does not have permissions to read the referenced document */
'inputs.reference.error.missing-read-permissions-title': 'Manglende tillatelser',

/** Error description for when the user does not have permissions to read the referenced document */
'inputs.reference.error.missing-read-permissions-description':
'Det refererte dokumentet kunne ikke åpnes på grunn av manglende tillatelser',

/** Error title for when the document is unavailable (for any possible reason) */
'inputs.reference.error.document-unavailable-title': 'Dokument ikke tilgjengelig',

/** Error title for when the reference search returned a document that is not an allowed type for the field */
'inputs.reference.error.invalid-search-result-type-title': `Søket returnerte en type som ikke er gyldig for denne referansen: "{{returnedType}}"`,

/** Error title for when the document referenced is not one of the types declared as allowed target types in schema */
'inputs.reference.error.invalid-type-title': 'Dokument av ugyldig type',

/** Error description for when the document referenced is not one of the types declared as allowed target types in schema */
'inputs.reference.error.invalid-type-description': `Referert dokument (<Code>{{documentId}}</Code>) er av type <Code>{{actualType}}</Code>. Ifølge skjemaet kan refererte dokumenter bare være av type <AllowedTypes />.`,

/** Placeholder shown in a reference input with no current value */
'inputs.reference.search-placeholder': 'Skriv for å søke',

/** Message shown when no documents were found that matched the given search string */
'inputs.reference.no-results-for-query':
'Ingen resultater for <SearchTerm>«{{searchTerm}}»</SearchTerm>',

/** Label for action to create a new document from the reference input */
'inputs.reference.action.create-new-document': 'Opprett ny',

/** Label for action to create a new document from the reference input, when there are multiple templates or document types to choose from */
'inputs.reference.action-create-new-document-select': 'Opprett ny…',

/** Label for action to clear the current value of the reference field */
'inputs.reference.action.clear': 'Tøm',

/** Label for action to replace the current value of the field */
'inputs.reference.action.replace': 'Erstatt',

/** Label for action to remove the reference from an array */
'inputs.reference.action.remove': 'Fjern',

/** Label for action to duplicate the current item to a new item (used within arrays) */
'inputs.reference.action.duplicate': 'Dupliser',

/** Label for action to cancel a previously initiated replace action */
'inputs.reference.action.replace-cancel': 'Avbryt erstatning',

/** Label for action that opens the referenced document in a new tab */
'inputs.reference.action.open-in-new-tab': 'Åpne i ny fane',

/** Text for tooltip showing when a document was published, using relative time (eg "how long ago was it published?") */
'inputs.reference.preview.published-at-time': 'Publisert <TimeAgo/>',

/** Text for tooltip indicating that a document has not yet been published */
'inputs.reference.preview.not-published': 'Ikke publisert',

/** Accessibility label for icon indicating that document has a published version */
'inputs.reference.preview.is-published-aria-label': 'Publisert',

/** Accessibility label for icon indicating that document does _not_ have a published version */
'inputs.reference.preview.is-not-published-aria-label': 'Ikke publisert',

/** Text for tooltip showing when a document was edited, using relative time (eg "how long ago was it edited?") */
'inputs.reference.preview.edited-at-time': 'Redigert <TimeAgo/>',

/** Text for tooltip indicating that a document has no unpublished edits */
'inputs.reference.preview.no-unpublished-edits': 'Ingen upubliserte endringer',

/** Accessibility label for icon indicating that document has unpublished changes */
'inputs.reference.preview.has-unpublished-changes-aria-label': 'Redigert',

/** Accessibility label for icon indicating that document does _not_ have any unpublished changes */
'inputs.reference.preview.has-no-unpublished-changes-aria-label': 'Ingen upubliserte endringer',
}

export default studioResources
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React, {ComponentProps} from 'react'
import {AddIcon} from '@sanity/icons'
import {Box, Button, Menu, MenuButton, MenuButtonProps, MenuItem, Tooltip} from '@sanity/ui'
import {useTranslation} from '../../../i18n'
import {InsufficientPermissionsMessage} from '../../../components'
import {CreateReferenceOption} from './types'
import type {CreateReferenceOption} from './types'

interface Props extends ComponentProps<typeof Button> {
id: string
Expand All @@ -27,6 +28,7 @@ const POPOVER_PROPS: MenuButtonProps['popover'] = {
export function CreateButton(props: Props) {
const {createOptions, onCreate, id, ...rest} = props

const {t} = useTranslation()
const canCreateAny = createOptions.some((option) => option.permission.granted)
if (!canCreateAny) {
return (
Expand All @@ -39,7 +41,13 @@ export function CreateButton(props: Props) {
>
{/* this wrapper div is needed because disabled button doesn't trigger mouse events */}
<div style={INLINE_BLOCK_STYLE}>
<Button text="Create new" mode="ghost" disabled icon={AddIcon} style={FULL_WIDTH} />
<Button
text={t('inputs.reference.create-new-document')}
mode="ghost"
disabled
icon={AddIcon}
style={FULL_WIDTH}
/>
</div>
</Tooltip>
)
Expand All @@ -51,7 +59,7 @@ export function CreateButton(props: Props) {
<Button
{...rest}
disabled={props.readOnly}
text="Create new…"
text={t('inputs.reference.action-create-new-document-select')}
mode="ghost"
icon={AddIcon}
/>
Expand Down Expand Up @@ -88,7 +96,7 @@ export function CreateButton(props: Props) {
) : (
<Button
{...rest}
text="Create new"
text={t('inputs.reference.action-create-new-document')}
mode="ghost"
disabled={!createOptions[0].permission.granted || props.readOnly}
onClick={() => onCreate(createOptions[0])}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
/* eslint-disable max-nested-callbacks */
/* eslint-disable no-nested-ternary */

import React from 'react'
import {ReferenceSchemaType} from '@sanity/types'
import {Stack, Text, TextSkeleton} from '@sanity/ui'
import {Observable} from 'rxjs'
import type {ReferenceSchemaType} from '@sanity/types'
import type {Observable} from 'rxjs'
import {Alert} from '../../components/Alert'
import {RenderPreviewCallback} from '../../types'
import {ReferenceInfo} from './types'
import {useTranslation} from '../../../i18n'
import type {RenderPreviewCallback} from '../../types'
import type {ReferenceInfo} from './types'
import {useReferenceInfo} from './useReferenceInfo'
import {ReferencePreview} from './ReferencePreview'

Expand All @@ -23,6 +21,7 @@ export function OptionPreview(props: {
}) {
const {getReferenceInfo, id: documentId, renderPreview} = props
const {isLoading, result: referenceInfo, error} = useReferenceInfo(documentId, getReferenceInfo)
const {t} = useTranslation()

if (isLoading) {
return (
Expand All @@ -36,7 +35,7 @@ export function OptionPreview(props: {
if (error) {
return (
<Stack space={2} padding={1}>
<Alert title="Failed to load referenced document">
<Alert title={t('inputs.reference.error.failed-to-load-document-title')}>
<Text muted size={1}>
Error: {error.message}
</Text>
Expand All @@ -52,7 +51,7 @@ export function OptionPreview(props: {
if (referenceInfo.availability.reason === 'PERMISSION_DENIED') {
return (
<Stack space={2} padding={1}>
Insufficient permissions to view this document
{t('inputs.reference.error.missing-read-permissions-description')}
</Stack>
)
}
Expand All @@ -62,7 +61,9 @@ export function OptionPreview(props: {
if (!refType) {
return (
<Stack space={2} padding={1}>
Search returned a type that's not valid for this reference: "${referenceInfo.type}"
{t('inputs.reference.error.invalid-search-result-type-title', {
returnedType: referenceInfo.type,
})}
</Stack>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {Box, Flex, Inline, Label, Stack, Text, Tooltip} from '@sanity/ui'
import {AccessDeniedIcon, HelpCircleIcon} from '@sanity/icons'
import {RenderPreviewCallback} from '../../types'
import {SanityDefaultPreview} from '../../../preview'
import {Translate, useIntlListFormat, useTranslation} from '../../../i18n'
import {TextWithTone} from '../../../components'
import {ReferencePreview} from './ReferencePreview'
import {Loadable} from './useReferenceInfo'
Expand All @@ -17,6 +18,7 @@ export function PreviewReferenceValue(props: {
showTypeLabel?: boolean
}) {
const {referenceInfo, renderPreview, type, value, showTypeLabel} = props
const {t} = useTranslation()

if (referenceInfo.isLoading || referenceInfo.error) {
return <SanityDefaultPreview isPlaceholder />
Expand Down Expand Up @@ -80,7 +82,7 @@ export function PreviewReferenceValue(props: {
<Box padding={1}>
<Flex align="center">
<Box flex={1} paddingY={2}>
<Text muted>Document unavailable</Text>
<Text muted>{t('inputs.reference.error.document-unavailable-title')}</Text>
</Box>
</Flex>
</Box>
Expand All @@ -90,14 +92,23 @@ export function PreviewReferenceValue(props: {
portal
content={
notFound ? (
<UnavailableMessage title="Not found" icon={HelpCircleIcon}>
The referenced document does not exist
<br />
(id: <code>{value._ref}</code>)
<UnavailableMessage
title={t('inputs.reference.error.nonexistent-document-title')}
icon={HelpCircleIcon}
>
<Translate
i18nKey="inputs.reference.error.nonexistent-document-description"
t={t}
components={{Code: ({children}) => <code>{children}</code>}}
values={{documentId: value._ref}}
/>
</UnavailableMessage>
) : (
<UnavailableMessage title="Insufficient permissions" icon={AccessDeniedIcon}>
The referenced document could not be accessed due to insufficient permissions
<UnavailableMessage
title={t('inputs.reference.error.missing-read-permissions-title')}
icon={AccessDeniedIcon}
>
{t('inputs.reference.error.missing-read-permissions-description')}
</UnavailableMessage>
)
}
Expand Down Expand Up @@ -163,13 +174,23 @@ function UnavailableMessage(props: {icon: ComponentType; children: ReactNode; ti
)
}

function InvalidType(props: {documentId: string; actualType: string; declaredTypes: string[]}) {
function InvalidType({
declaredTypes,
documentId,
actualType,
}: {
documentId: string
actualType: string
declaredTypes: string[]
}) {
const {t} = useTranslation()

return (
<Flex align="center" justify="flex-start">
<Box padding={1}>
<Flex align="center">
<Box flex={1} paddingY={2}>
<Text muted>Document of invalid type</Text>
<Text muted>{t('inputs.reference.error.invalid-type-title')}</Text>
</Box>
</Flex>
</Box>
Expand All @@ -179,16 +200,15 @@ function InvalidType(props: {documentId: string; actualType: string; declaredTyp
content={
<Stack space={3} padding={3}>
<Text size={1}>
Referenced document (<code>{props.documentId}</code>) is of type{' '}
<code>{props.actualType}</code>.
</Text>
<Text size={1}>
According to the schema, referenced documents can only be of type{' '}
{humanizeList(
props.declaredTypes.map((typeName) => <code key={typeName}>{typeName}</code>),
'or',
)}
.
<Translate
t={t}
i18nKey="inputs.reference.error.invalid-type-description"
values={{documentId, actualType}}
components={{
AllowedTypes: () => <HumanizedList values={declaredTypes} />,
Code: ({children}) => <code>{children}</code>,
}}
/>
</Text>
</Stack>
}
Expand All @@ -204,20 +224,18 @@ function InvalidType(props: {documentId: string; actualType: string; declaredTyp
)
}

const humanizeList = (list: React.ReactNode[], conjunction: string) => {
if (list.length === 1) {
return list[0]
}

if (list.length === 2) {
return [list[0], <Fragment key="comma"> {conjunction} </Fragment>, list[1]]
}

const subList = list.slice(0, -1)
return [
...subList.map((item, i) => <Fragment key={i}>{item}, </Fragment>),
<Fragment key="last">
{conjunction} {list[list.length - 1]}
</Fragment>,
]
function HumanizedList(props: {values: string[]}) {
const listFormat = useIntlListFormat({type: 'disjunction'})
const parts = listFormat.formatToParts(props.values)
return (
<Fragment>
{parts.map((segment) =>
segment.type === 'element' ? (
<code key={segment.value}>{segment.value}</code>
) : (
segment.value
),
)}
</Fragment>
)
}
Loading