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): localize global search filters and results #5108

Merged
merged 7 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
419 changes: 370 additions & 49 deletions dev/test-studio/plugins/locale-no-nb/bundles/studio.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"deploy:test": "yarn build && cd dev/test-studio && sanity deploy",
"dev": "yarn start",
"dev:i18n": "SANITY_STUDIO_DEBUG_I18N=true yarn start",
"dev:i18n:reverse": "SANITY_STUDIO_DEBUG_I18N=reverse yarn start",
"dev:design-studio": "yarn --cwd dev/design-studio dev",
"dev:starter-studio": "yarn --cwd dev/starter-studio dev",
"dev:strict-studio": "yarn --cwd dev/strict-studio dev",
Expand Down
55 changes: 27 additions & 28 deletions packages/sanity/src/core/form/inputs/DateInputs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,36 @@ export function getCalendarLabels(
t: (key: string, values?: Record<string, unknown>) => string,
): CalendarLabels {
return {
goToNextMonth: t('inputs.datetime.calendar.action.go-to-next-month'),
goToPreviousMonth: t('inputs.datetime.calendar.action.go-to-previous-month'),
goToNextYear: t('inputs.datetime.calendar.action.go-to-next-year'),
goToPreviousYear: t('inputs.datetime.calendar.action.go-to-previous-year'),
setToCurrentTime: t('inputs.datetime.calendar.action.set-to-current-time'),
selectHour: t('inputs.datetime.calendar.action.select-hour'),
selectMinute: t('inputs.datetime.calendar.action.select-minute'),
goToNextMonth: t('calendar.action.go-to-next-month'),
goToPreviousMonth: t('calendar.action.go-to-previous-month'),
goToNextYear: t('calendar.action.go-to-next-year'),
goToPreviousYear: t('calendar.action.go-to-previous-year'),
setToCurrentTime: t('calendar.action.set-to-current-time'),
selectHour: t('calendar.action.select-hour'),
selectMinute: t('calendar.action.select-minute'),
monthNames: [
t('inputs.datetime.calendar.month-names.january'),
t('inputs.datetime.calendar.month-names.february'),
t('inputs.datetime.calendar.month-names.march'),
t('inputs.datetime.calendar.month-names.april'),
t('inputs.datetime.calendar.month-names.may'),
t('inputs.datetime.calendar.month-names.june'),
t('inputs.datetime.calendar.month-names.july'),
t('inputs.datetime.calendar.month-names.august'),
t('inputs.datetime.calendar.month-names.september'),
t('inputs.datetime.calendar.month-names.october'),
t('inputs.datetime.calendar.month-names.november'),
t('inputs.datetime.calendar.month-names.december'),
t('calendar.month-names.january'),
t('calendar.month-names.february'),
t('calendar.month-names.march'),
t('calendar.month-names.april'),
t('calendar.month-names.may'),
t('calendar.month-names.june'),
t('calendar.month-names.july'),
t('calendar.month-names.august'),
t('calendar.month-names.september'),
t('calendar.month-names.october'),
t('calendar.month-names.november'),
t('calendar.month-names.december'),
],
weekDayNamesShort: [
t('inputs.datetime.calendar.weekday-names.short.monday'),
t('inputs.datetime.calendar.weekday-names.short.tuesday'),
t('inputs.datetime.calendar.weekday-names.short.wednesday'),
t('inputs.datetime.calendar.weekday-names.short.thursday'),
t('inputs.datetime.calendar.weekday-names.short.friday'),
t('inputs.datetime.calendar.weekday-names.short.saturday'),
t('inputs.datetime.calendar.weekday-names.short.sunday'),
t('calendar.weekday-names.short.monday'),
t('calendar.weekday-names.short.tuesday'),
t('calendar.weekday-names.short.wednesday'),
t('calendar.weekday-names.short.thursday'),
t('calendar.weekday-names.short.friday'),
t('calendar.weekday-names.short.saturday'),
t('calendar.weekday-names.short.sunday'),
],
setToTimePreset: (time, date) =>
t('inputs.datetime.calendar.action.set-to-time-preset', {time, date}),
setToTimePreset: (time, date) => t('calendar.action.set-to-time-preset', {time, date}),
}
}
165 changes: 165 additions & 0 deletions packages/sanity/src/core/hooks/__tests__/useUnitFormatter.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import React, {type ReactElement} from 'react'
import {ThemeProvider, studioTheme} from '@sanity/ui'
import {renderHook} from '@testing-library/react'
import {LocaleProviderBase, usEnglishLocale} from '../../i18n'
import {prepareI18n} from '../../i18n/i18nConfig'
import {studioDefaultLocaleResources} from '../../i18n/bundles/studio'
import {FormattableMeasurementUnit, useUnitFormatter} from '../useUnitFormatter'

describe('useUnitFormatter', () => {
const {i18next} = prepareI18n({
projectId: 'test',
dataset: 'test',
name: 'test',
i18n: {bundles: [studioDefaultLocaleResources]},
})

const wrapper = ({children}: {children: ReactElement}) => (
<ThemeProvider theme={studioTheme}>
<LocaleProviderBase
locales={[usEnglishLocale, {id: 'fr-FR', title: 'Français'}]}
i18next={i18next}
projectId="test"
sourceId="test"
>
{children}
</LocaleProviderBase>
</ThemeProvider>
)

beforeAll(() => i18next.init())
beforeEach(() => i18next.changeLanguage('en-US'))

it('formats with long units as default', () => {
const {result} = renderHook(() => useUnitFormatter()(1, 'meter'), {wrapper})
expect(result.current).toBe('1 meter')
})

it('formats singular/plural correctly', () => {
const {result} = renderHook(() => useUnitFormatter()(2, 'meter'), {wrapper})
expect(result.current).toBe('2 meters')
})

it('can be configured to use short units', () => {
const {result} = renderHook(() => useUnitFormatter({unitDisplay: 'short'})(13, 'foot'), {
wrapper,
})
expect(result.current).toBe('13 ft')
})

it('can be configured to use narrow units', () => {
const {result} = renderHook(() => useUnitFormatter({unitDisplay: 'narrow'})(13, 'foot'), {
wrapper,
})
expect(result.current).toBe('13′')
})

it('respects active locale', async () => {
await i18next.changeLanguage('fr-FR')
const {result} = renderHook(() => useUnitFormatter()(2, 'meter'), {wrapper})
expect(result.current).toBe('2 mètres')
})

it('can format all defined units', () => {
const {
result: {current: format},
} = renderHook(() => useUnitFormatter({unitDisplay: 'short'}), {wrapper})

const formatted: Partial<Record<FormattableMeasurementUnit, string>> = {}
const units: FormattableMeasurementUnit[] = [
'acre',
'bit',
'byte',
'celsius',
'centimeter',
'day',
'degree',
'fahrenheit',
'fluid-ounce',
'foot',
'gallon',
'gigabit',
'gigabyte',
'gram',
'hectare',
'hour',
'inch',
'kilobit',
'kilobyte',
'kilogram',
'kilometer',
'liter',
'megabit',
'megabyte',
'meter',
'mile',
'mile-scandinavian',
'milliliter',
'millimeter',
'millisecond',
'minute',
'month',
'ounce',
'percent',
'petabyte',
'pound',
'second',
'stone',
'terabit',
'terabyte',
'week',
'yard',
'year',
]

units.forEach((unit, idx) => {
formatted[unit] = format(idx, unit)
})

expect(formatted).toMatchObject({
acre: '0 ac',
bit: '1 bit',
byte: '2 byte',
celsius: '3°C',
centimeter: '4 cm',
day: '5 days',
degree: '6 deg',
fahrenheit: '7°F',
'fluid-ounce': '8 fl oz',
foot: '9 ft',
gallon: '10 gal',
gigabit: '11 Gb',
gigabyte: '12 GB',
gram: '13 g',
hectare: '14 ha',
hour: '15 hr',
inch: '16 in',
kilobit: '17 kb',
kilobyte: '18 kB',
kilogram: '19 kg',
kilometer: '20 km',
liter: '21 L',
megabit: '22 Mb',
megabyte: '23 MB',
meter: '24 m',
mile: '25 mi',
'mile-scandinavian': '26 smi',
milliliter: '27 mL',
millimeter: '28 mm',
millisecond: '29 ms',
minute: '30 min',
month: '31 mths',
ounce: '32 oz',
percent: '33%',
petabyte: '34 PB',
pound: '35 lb',
second: '36 sec',
stone: '37 st',
terabit: '38 Tb',
terabyte: '39 TB',
week: '40 wks',
yard: '41 yd',
year: '42 yrs',
})
})
})
1 change: 1 addition & 0 deletions packages/sanity/src/core/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export * from './useSyncState'
export * from './useTemplates'
export * from './useTimeAgo'
export * from './useTools'
export * from './useUnitFormatter'
export * from './useValidationStatus'
99 changes: 99 additions & 0 deletions packages/sanity/src/core/hooks/useUnitFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {intlCache} from '../i18n/intlCache'
import {useCurrentLocale} from '../i18n/hooks/useLocale'

/**
* Options for the `useUnitFormatter` hook
*
* @public
*/
export type UseUnitFormatterOptions = Omit<Intl.NumberFormatOptions, 'unit'>

/**
* Available measurement units
*
* @public
*/
export type FormattableMeasurementUnit =
| 'acre'
| 'bit'
| 'byte'
| 'celsius'
| 'centimeter'
| 'day'
| 'degree'
| 'fahrenheit'
| 'fluid-ounce'
| 'foot'
| 'gallon'
| 'gigabit'
| 'gigabyte'
| 'gram'
| 'hectare'
| 'hour'
| 'inch'
| 'kilobit'
| 'kilobyte'
| 'kilogram'
| 'kilometer'
| 'liter'
| 'megabit'
| 'megabyte'
| 'meter'
| 'mile'
| 'mile-scandinavian'
| 'milliliter'
| 'millimeter'
| 'millisecond'
| 'minute'
| 'month'
| 'ounce'
| 'percent'
| 'petabyte'
| 'pound'
| 'second'
| 'stone'
| 'terabit'
| 'terabyte'
| 'week'
| 'yard'
| 'year'

/**
* Returns a formatter with the given options. Function takes a number and the unit to format as
* the second argument. The formatter will yield localized output, based on the users' selected
* locale.
*
* This differs from regular `Intl.NumberFormat` in two ways:
* 1. You do not need to instantiate a new formatter for each unit you want to format
* (still happens behind the scenes, but is memoized)
* 2. The default unit display style (`unitDisplay`) is `long`
*
* @example
* ```ts
* function MyComponent() {
* const format = useUnitFormatter()
* return <div>{format(2313, 'meter')}</div>
* // en-US -> 2,313 meters
* // fr-FR -> 2 313 mètres
* }
* ```
*
* @param options - Optional options for the unit formatter
* @returns Formatter function
* @public
*/
export function useUnitFormatter(
options: UseUnitFormatterOptions = {},
): (value: number, unit: FormattableMeasurementUnit) => string {
const currentLocale = useCurrentLocale()
const defaultOptions: Intl.NumberFormatOptions = {
unitDisplay: 'long',
...options,
style: 'unit',
}

return function format(value: number, unit: FormattableMeasurementUnit) {
const formatter = intlCache.numberFormat(currentLocale, {...defaultOptions, unit})
return formatter.format(value)
}
}
12 changes: 8 additions & 4 deletions packages/sanity/src/core/i18n/Translate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const RECOGNIZED_HTML_TAGS = [
'sup',
]

type ComponentMap = Record<
/**
* @beta
*/
export type TranslateComponentMap = Record<
string,
ComponentType<{children?: ReactNode}> | keyof JSX.IntrinsicElements
>
Expand All @@ -30,12 +33,13 @@ type ComponentMap = Record<
export interface TranslationProps {
t: TFunction
i18nKey: string

components?: TranslateComponentMap
context?: string
values?: Record<string, number | string | string[]>
components?: ComponentMap
values?: Record<string, string | string[] | number | undefined>
}

function render(tokens: Token[], componentMap: ComponentMap): ReactNode {
function render(tokens: Token[], componentMap: TranslateComponentMap): ReactNode {
const [head, ...tail] = tokens
if (!head) {
return null
Expand Down
Loading
Loading