import { renderHook } from '@testing-library/react'
import * as React from 'react'
import { describe, expect, it, vi } from 'vitest'
import { TLUiTranslation } from './translations'
import {
TranslationsContext,
useCurrentTranslation,
useDirection,
useMaybeCurrentTranslation,
useTranslation,
} from './useTranslation'
// The repo-wide test setup mocks this module's `useCurrentTranslation` to always succeed, since
// most tests don't care about translation context. Unmock it here so we can test its real
// context-dependent behavior.
vi.unmock('./useTranslation')
const catalog = {
'action.copy': 'Copy',
'a11y.shape-index': '{num} of {total}',
'comments.replies': '{count, plural, one {# reply} other {# replies}}',
}
const rtlTranslation: TLUiTranslation = {
locale: 'ar',
label: 'Arabic',
dir: 'rtl',
messages: catalog as TLUiTranslation['messages'],
}
function withTranslation(translation: TLUiTranslation) {
return ({ children }: { children: React.ReactNode }) => (
{children}
)
}
describe('useCurrentTranslation', () => {
it('returns the current translation when inside a provider', () => {
const { result } = renderHook(() => useCurrentTranslation(), {
wrapper: withTranslation(rtlTranslation),
})
expect(result.current).toBe(rtlTranslation)
})
it('throws when used outside of a provider', () => {
expect(() => renderHook(() => useCurrentTranslation())).toThrow(
'useCurrentTranslation must be used inside of '
)
})
})
describe('useMaybeCurrentTranslation', () => {
it('returns the current translation when inside a provider', () => {
const { result } = renderHook(() => useMaybeCurrentTranslation(), {
wrapper: withTranslation(rtlTranslation),
})
expect(result.current).toBe(rtlTranslation)
})
it('returns null when used outside of a provider, without throwing', () => {
const { result } = renderHook(() => useMaybeCurrentTranslation())
expect(result.current).toBeNull()
})
})
describe('useDirection', () => {
it('returns the direction from the current translation when inside a provider', () => {
const { result } = renderHook(() => useDirection(), {
wrapper: withTranslation(rtlTranslation),
})
expect(result.current).toBe('rtl')
})
it('falls back to "ltr" when used outside of a provider, without throwing', () => {
const { result } = renderHook(() => useDirection())
expect(result.current).toBe('ltr')
})
})
describe('useTranslation', () => {
function msgWith(messages: Record, locale = 'en') {
const { result } = renderHook(() => useTranslation(), {
wrapper: withTranslation({
locale,
label: locale,
dir: 'ltr',
messages: { ...catalog, ...messages } as TLUiTranslation['messages'],
}),
})
return result.current
}
it('looks up a plain message', () => {
expect(msgWith({})('action.copy')).toBe('Copy')
})
it('substitutes values into a placeholder', () => {
expect(msgWith({})('a11y.shape-index', { num: 2, total: 7 })).toBe('2 of 7')
})
it('selects a plural category from the locale, not from the value alone', () => {
const msg = msgWith({})
expect(msg('comments.replies', { count: 1 })).toBe('1 reply')
expect(msg('comments.replies', { count: 4 })).toBe('4 replies')
})
// Apps are documented to pass their own keys and plain text through `msg`.
it('returns an unknown key as-is', () => {
expect(msgWith({})('my.own.key')).toBe('my.own.key')
expect(msgWith({ 'my.own.key': 'Mine' })('my.own.key')).toBe('Mine')
})
it('renders an empty message as empty, not as its id', () => {
expect(msgWith({ 'action.copy': '' })('action.copy')).toBe('')
})
it('passes a nullish id straight through', () => {
expect(msgWith({})(undefined)).toBeUndefined()
})
// An app's override is not necessarily valid ICU. A stray brace must not take down the UI.
it('falls back to the raw string when an override is malformed', () => {
expect(msgWith({ 'my.own.key': 'Cost: {' })('my.own.key', { n: 1 })).toBe('Cost: {')
})
})