Skip to content

Commit

Permalink
feat: add support for language option
Browse files Browse the repository at this point in the history
  • Loading branch information
marsidev committed Mar 6, 2023
1 parent ff91826 commit b0b7b02
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 30 deletions.
24 changes: 13 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,19 @@ function Widget() {


### Render options
| **Option** | **Type** | **Default** | **Description** |
| ----------------- | --------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| theme | `string` | `'auto'` | The widget theme. You can choose between `light`, `dark` or `auto`. |
| tabIndex | `number` | `0` | The `tabindex` of Turnstile’s iframe for accessibility purposes. |
| action | `string` | `undefined` | A customer value that can be used to differentiate widgets under the same `sitekey` in analytics and which is returned upon validation. This can only contain up to 32 alphanumeric characters including `_` and `-`. |
| cData | `string` | `undefined` | A customer payload that can be used to attach customer data to the challenge throughout its issuance and which is returned upon validation. This can only contain up to 255 alphanumeric characters including `_` and `-`. |
| responseField | `boolean` | `true` | A boolean that controls if an input element with the response token is created. |
| responseFieldName | `string` | `'cf-turnstile-response'` | Name of the input element. |
| size | `string` | `'normal'` | The widget size. Can take the following values: `'normal'`, `'compact'`, or `'invisible'`. The normal size is 300x65px, the compact size is 130x120px. Use `invisible` if your key type is `invisible`, this option will prevent creating placeholder for the widget. |
| retry | `string` | `'auto'` | Controls whether the widget should automatically retry to obtain a token if it did not succeed. The default is `'auto'`, which will retry automatically. This can be set to `'never'` to disable retry upon failure. |
| retryInterval | `number` | `8000` | When `retry` is set to `'auto'`, `retryInterval` controls the time between retry attempts in milliseconds. The value must be a positive integer less than `900000`. When `retry` is set to `'never'`, this parameter has no effect. |

| **Option** | **Type** | **Default** | **Description** |
| ----------------- | --------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| theme | `string` | `'auto'` | The widget theme. You can choose between `light`, `dark` or `auto`. |
| language | `string` | `'auto'` | Language to display, must be either: `auto` (default) to use the language that the visitor has chosen, or an ISO 639-1 two-letter language code (e.g. `en`) or language and country code (e.g. `pt-BR`). The following languages are currently supported: `ar-EG`, `de`, `en`, `es`, `fa`, `fr`, `id`, `it`, `ja`, `ko`, `nl`, `pl`, `pt-BR`, `ru`, `tr`, `zh-CN` and `zh-TW`. |
| tabIndex | `number` | `0` | The `tabindex` of Turnstile’s iframe for accessibility purposes. |
| action | `string` | `undefined` | A customer value that can be used to differentiate widgets under the same `sitekey` in analytics and which is returned upon validation. This can only contain up to 32 alphanumeric characters including `_` and `-`. |
| cData | `string` | `undefined` | A customer payload that can be used to attach customer data to the challenge throughout its issuance and which is returned upon validation. This can only contain up to 255 alphanumeric characters including `_` and `-`. |
| responseField | `boolean` | `true` | A boolean that controls if an input element with the response token is created. |
| responseFieldName | `string` | `'cf-turnstile-response'` | Name of the input element. |
| size | `string` | `'normal'` | The widget size. Can take the following values: `'normal'`, `'compact'`, or `'invisible'`. The normal size is 300x65px, the compact size is 130x120px. Use `invisible` if your key type is `invisible`, this option will prevent creating placeholder for the widget. |
| retry | `string` | `'auto'` | Controls whether the widget should automatically retry to obtain a token if it did not succeed. The default is `'auto'`, which will retry automatically. This can be set to `'never'` to disable retry upon failure. |
| retryInterval | `number` | `8000` | When `retry` is set to `'auto'`, `retryInterval` controls the time between retry attempts in milliseconds. The value must be a positive integer less than `900000`. When `retry` is set to `'never'`, this parameter has no effect. |

> All this options are optional.
Expand Down
26 changes: 15 additions & 11 deletions packages/example/src/components/ConfigForm.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { forwardRef, useState } from 'react'
import { LangOptions } from '../constants'
import Options from './Options'

interface FormProps {
onChangeTheme: (value: string) => void
onChangeSiteKeyType: (value: string) => void
onChangeSize: (value: string) => void
onChangeLang: (value: string) => void
}

const ThemeOptions = [
Expand All @@ -31,7 +33,8 @@ type SiteKeyType = typeof SiteKeyOptions[number]['value']
const ConfigForm = forwardRef<HTMLFormElement, FormProps>((props, ref) => {
const [sizeType, setSizeType] = useState<SizeType>('normal')
const [siteKeyType, setSiteKeyType] = useState<SiteKeyType>('pass')
const isInvisibleType = (sizeType === 'invisible')

const isInvisibleType = sizeType === 'invisible'

function onChangeSiteKeyTypeProxy(val: string) {
setSiteKeyType(val as SiteKeyType)
Expand All @@ -41,7 +44,7 @@ const ConfigForm = forwardRef<HTMLFormElement, FormProps>((props, ref) => {
function onChangeSizeProxy(val: string) {
if (val === 'invisible' && siteKeyType === 'interactive') {
// Change the siteKey type to `pass` when the user choose the invisible
// widget type. Will prevent interactive challenge being choosen on
// widget type. Will prevent interactive challenge being chosen on
// invisible widget.
onChangeSiteKeyTypeProxy('pass')
}
Expand All @@ -59,12 +62,7 @@ const ConfigForm = forwardRef<HTMLFormElement, FormProps>((props, ref) => {
onChange={props.onChangeTheme}
/>

<Options
name='size'
options={[...SizeOptions]}
title='Size'
onChange={onChangeSizeProxy}
/>
<Options name='size' options={[...SizeOptions]} title='Size' onChange={onChangeSizeProxy} />

<Options
helperUrl='https://developers.cloudflare.com/turnstile/frequently-asked-questions/#are-there-sitekeys-and-secret-keys-that-can-be-used-for-testing'
Expand All @@ -73,13 +71,19 @@ const ConfigForm = forwardRef<HTMLFormElement, FormProps>((props, ref) => {
...option,
// Option will be disabled when requesting interactive challenge on
// invisible widget type
disabled: (option.value === 'interactive' && isInvisibleType)
})
)}
disabled: option.value === 'interactive' && isInvisibleType
}))}
title='Demo Site Key Type'
value={siteKeyType}
onChange={onChangeSiteKeyTypeProxy}
/>

<Options
name='lang'
options={[...LangOptions]}
title='Language'
onChange={props.onChangeLang}
/>
</div>
</form>
)
Expand Down
16 changes: 15 additions & 1 deletion packages/example/src/components/Demo.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useRef, useState } from 'react'
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
import { LangOptions } from '../constants'
import ConfigForm from './ConfigForm'
import StateLabels from './StateLabels'
import WidgetMethods from './WidgetMethods'
Expand All @@ -17,14 +18,17 @@ type Theme = 'light' | 'dark' | 'auto'
type Size = 'normal' | 'compact'
export type WidgetStatus = 'solved' | 'error' | 'expired' | null
type SiteKeyType = keyof typeof DEMO_SITEKEY
type LangType = typeof LangOptions[number]['value']

const Demo = () => {
const [theme, setTheme] = useState<Theme>('auto')
const [size, setSize] = useState<Size>('normal')
const [siteKeyType, setSiteKeyType] = useState<SiteKeyType>('pass')
const [status, setStatus] = useState<WidgetStatus>(null)
const [lang, setLang] = useState<LangType>('auto')
const [token, setToken] = useState<string>()
const [rerenderCount, setRerenderCount] = useState(0)

const configFormRef = useRef<HTMLFormElement>(null)
const turnstileRef = useRef<TurnstileInstance>(null)
const testingSiteKey = DEMO_SITEKEY[siteKeyType]
Expand All @@ -51,6 +55,11 @@ const Demo = () => {
onRestartStates()
}

const onChangeLang = (value: string) => {
setLang(value as LangType)
onRestartStates()
}

const onSuccess = (token: string) => {
setToken(token)
setStatus('solved')
Expand All @@ -69,7 +78,11 @@ const Demo = () => {
<Turnstile
ref={turnstileRef}
autoResetOnExpire={false}
options={{ theme, size }}
options={{
theme,
size,
language: lang
}}
siteKey={testingSiteKey}
onError={() => setStatus('error')}
onExpire={onExpire}
Expand All @@ -79,6 +92,7 @@ const Demo = () => {
<h2 className='font-semibold text-2xl mt-8'>Configuration</h2>
<ConfigForm
ref={configFormRef}
onChangeLang={onChangeLang}
onChangeSiteKeyType={onChangeSiteKeyType}
onChangeSize={onChangeSize}
onChangeTheme={onChangeTheme}
Expand Down
22 changes: 22 additions & 0 deletions packages/example/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const LangOptions = [
{ label: 'Auto', value: 'auto' },
{ label: 'العربية', value: 'ar' },
{ label: 'العربية (مصر)', value: 'ar-EG' },
{ label: 'Deutsch', value: 'de' },
{ label: 'English', value: 'en' },
{ label: 'Español', value: 'es' },
{ label: 'فارسی', value: 'fa' },
{ label: 'Français', value: 'fr' },
{ label: 'Bahasa Indonesia', value: 'id' },
{ label: 'Italiano', value: 'it' },
{ label: '日本語', value: 'ja' },
{ label: '한국어', value: 'ko' },
{ label: 'Nederlands', value: 'nl' },
{ label: 'Polski', value: 'pl' },
{ label: 'Português', value: 'pt' },
{ label: 'Português (Brasil)', value: 'pt-BR' },
{ label: 'Русский', value: 'ru' },
{ label: 'Türkçe', value: 'tr' },
{ label: '中文(简体)', value: 'zh-CN' },
{ label: '繁體中文', value: 'zh-TW' }
] as const
3 changes: 2 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const config: PlaywrightTestConfig = {
use: {
trace: 'retry-with-trace',
headless: true,
baseURL
baseURL,
locale: 'en-US'
},

projects: [
Expand Down
13 changes: 10 additions & 3 deletions src/lib.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { CONTAINER_STYLE_SET, DEFAULT_CONTAINER_ID, DEFAULT_ONLOAD_NAME, getTurnstileSizeOpts, injectTurnstileScript } from './utils'
import {
CONTAINER_STYLE_SET,
DEFAULT_CONTAINER_ID,
DEFAULT_ONLOAD_NAME,
getTurnstileSizeOpts,
injectTurnstileScript
} from './utils'
import { RenderOptions, TurnstileInstance, TurnstileProps } from './types'

export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProps>((props, ref) => {
Expand Down Expand Up @@ -68,7 +74,7 @@ export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProp
}

if (!containerRef.current) {
console.warn('Container has not rendered')
console.warn('The container has not been rendered yet')
return
}

Expand Down Expand Up @@ -99,7 +105,8 @@ export const Turnstile = forwardRef<TurnstileInstance | undefined, TurnstileProp
'response-field': config.responseField,
'response-field-name': config.responseFieldName,
retry: config.retry ?? 'auto',
'retry-interval': config.retryInterval ?? 8000
'retry-interval': config.retryInterval ?? 8000,
language: config.language ?? 'auto'
}

const onLoadScript = () => {
Expand Down
36 changes: 34 additions & 2 deletions src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,16 @@ interface RenderOptions {
* @default 8000
*/
'retry-interval'?: number
/**
* Language to display, must be either: `auto` (default) to use the language that the visitor has chosen, or an ISO 639-1 two-letter language code (e.g. `en`).
* @default `auto`
*/
language?: 'auto' | LangCode
}

/** Props needed for the `options` prop in the `<Turnstile />` component. */
interface ComponentRenderOptions
extends Pick<RenderOptions, 'action' | 'cData' | 'theme' | 'retry'> {
extends Pick<RenderOptions, 'action' | 'cData' | 'theme' | 'retry' | 'language'> {
/**
* The tabindex of Turnstile’s iframe for accessibility purposes.
* @default 0
Expand Down Expand Up @@ -234,4 +239,31 @@ type ContainerSizeSet = {
[size in NonNullable<ComponentRenderOptions['size']>]: React.CSSProperties
}

export type { TurnstileInstance, RenderOptions, TurnstileProps, InjectTurnstileScriptParams, ContainerSizeSet }
type LangCode =
| 'ar'
| 'ar-EG'
| 'de'
| 'en'
| 'es'
| 'fa'
| 'fr'
| 'id'
| 'it'
| 'ja'
| 'ko'
| 'nl'
| 'pl'
| 'pt'
| 'pt-BR'
| 'ru'
| 'tr'
| 'zh-CN'
| 'zh-TW'

export type {
TurnstileInstance,
RenderOptions,
TurnstileProps,
InjectTurnstileScriptParams,
ContainerSizeSet
}
27 changes: 26 additions & 1 deletion test/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,30 @@ test('widget can be sized', async () => {
const iframeAfter = page.frameLocator('iframe[src^="https://challenges.cloudflare.com"]')
const boxAfter = await iframeAfter.locator('body').boundingBox()
expect(boxAfter).toBeDefined()
expect(boxAfter!.width).toBe(130)

test('widget can change language', async () => {
// default lang 'auto' (en)
await ensureFrameVisible()
const iframe = page.frameLocator('iframe[src^="https://challenges.cloudflare.com"]')
await expect(iframe.locator('#success-text')).toContainText('Success!')

// change lang to 'es'
await page.getByLabel('Language').selectOption('es')
await ensureFrameVisible()
await expect(iframe.locator('#success-text')).toContainText('¡Operación exitosa!')

// change lang to 'de'
await page.getByLabel('Language').selectOption('de')
await ensureFrameVisible()
await expect(iframe.locator('#success-text')).toContainText('Erfolg!')

// change lang to 'ja'
await page.getByLabel('Language').selectOption('ja')
await ensureFrameVisible()
await expect(iframe.locator('#success-text')).toContainText('成功しました!')

// change lang to 'ru'
await page.getByLabel('Language').selectOption('ru')
await ensureFrameVisible()
await expect(iframe.locator('#success-text')).toContainText('Проверка пройдена!')
})

0 comments on commit b0b7b02

Please sign in to comment.