Skip to content

Commit

Permalink
#7 - Formio renderer: implement mechanism to provide translations.
Browse files Browse the repository at this point in the history
  • Loading branch information
Sven van de Scheur committed Mar 1, 2023
1 parent dd9faaa commit befb83f
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 27 deletions.
22 changes: 16 additions & 6 deletions src/components/textfield/textfield.component.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useHTMLRef } from '@hooks'
import { OF_PREFIX } from '@lib'
import { gettext, OF_PREFIX } from '@lib'
import { ComponentProps } from '@types'
import classNames from 'classnames'
import React, { useState } from 'react'
Expand All @@ -18,7 +18,6 @@ export const TextField = ({
components,
...props
}: ComponentProps) => {
// {"label":"Text Field","prefix":"","suffix":""," "hideLabel":false,"description":"Lorem ipsum dolor sit amet","defaultValue":"Standaardwaarde","labelPosition":"top","showCharCount":true,"showWordCount":false,"customDefaultValue":""}
const [pristineState, setPristineState] = useState<boolean>(true)
const [charCountState, setCharCountState] = useState<number>(
String(component.defaultValue).length
Expand Down Expand Up @@ -92,7 +91,7 @@ export const TextField = ({
return (
<div className={containerClassName} id={component.id} ref={componentRef} {...props}>
<label className={labelClassName} htmlFor={inputAttrs.id}>
{component.label}&nbsp;
{gettext(component.label, renderConfiguration.i18n)}&nbsp;
</label>

<div ref={elementRef}>
Expand All @@ -105,19 +104,30 @@ export const TextField = ({
{...callbacks}
/>
{component.showCharCount && !pristineState && (
<span className='charcount'>{charCountState} karakters</span>
<span className='charcount'>
{gettext('{{ count }} characters', renderConfiguration.i18n, { count: charCountState })}
</span>
)}
</div>

<div className='openforms-help-text'>{component.description}</div>
<div className='openforms-help-text'>
{gettext(component.description, renderConfiguration.i18n)}
</div>

<div
aria-describedby={component.id}
role='alert'
className={errorsClassName}
ref={messageContainerRef}
>
{errors.map((error) => error)}
{errors.map((error, index) => {
return (
<React.Fragment key={error}>
{index > 0 && <br />}
{gettext(error, renderConfiguration.i18n)}
</React.Fragment>
)
})}
</div>
</div>
)
Expand Down
4 changes: 2 additions & 2 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Columns, Content, Form, TextField } from '@components'
import { BaseRenderer } from '@lib'
import { TreeConfiguration, RenderConfiguration } from '@types'

export const DEFAULT_RENDER_CONFIGURATION: RenderConfiguration = {
export const BASE_RENDER_CONFIGURATION: RenderConfiguration = {
components: [
{
component: Form,
Expand All @@ -28,7 +28,7 @@ export const renderForm = ({
callbacks = {},
formErrors = {},
form,
renderConfiguration = DEFAULT_RENDER_CONFIGURATION
renderConfiguration = BASE_RENDER_CONFIGURATION
}: TreeConfiguration): React.ReactNode => {
return renderConfiguration.renderer.renderTree({
callbacks,
Expand Down
28 changes: 28 additions & 0 deletions src/lib/i18n/gettext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { I18N } from '@types'

/**
* Returns translation for key based on locale in i18n.
* @param {string} [key]
* @param {I18N} [i18n]
* @param {{[index: string]: any}} context {{ placeholder }} replacements.
* @param {string} [locale] If omitted: an attempt is made to resolve a local from the browser.
*/
export const gettext = (
key: string = '',
i18n: I18N = {},
context: { [index: string]: any } = {},
locale: string = ''
): string => {
const navigateLocales = navigator?.languages.map((locale) => locale.split('-')[0]) || []
const locales = [locale, ...navigateLocales, Object.keys(i18n)[0] || '']
const resolvedLocale = Object.keys(i18n).find((key) => locales.indexOf(key) > -1)
const dictionary = i18n[resolvedLocale as string] || {}
const translation = dictionary[key] || key

// Apply context.
return Object.entries(context).reduce(
(compiledTranslation, [key, value]) =>
compiledTranslation.replace(new RegExp(`{{\\s*?${key}\\s*?}}`, 'g'), String(value)),
translation
)
}
1 change: 1 addition & 0 deletions src/lib/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './gettext'
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './constants'
export * from './i18n'
export * from './renderer'
export * from './utils'
export * from './validation'
51 changes: 32 additions & 19 deletions src/lib/validation/validation.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import { gettext } from '../i18n'
import { Component, I18N } from '@types'
import { ValidateOptions } from 'formiojs'

/**
* Runs all frontend validators based on validate.
* @param {boolean|number|string} value
* @param {{maxLength: number, minLength: number, pattern: RegExp|string, required: boolean}} validate Form.io validation object.
* @param {Component} component
* @param {I18N} i18n
* @throws {ValidationError[]} As promise rejection if invalid.
* @return {Promise<boolean>} Promise for true if valid.
*/
export const validate = async (
value: boolean | number | string,
validate: { maxLength: number; minLength: number; pattern: RegExp | string; required: boolean }
component: Component,
i18n: I18N = {}
): Promise<boolean | void> => {
const { maxLength, minLength, pattern, required } = validate
const { maxLength, minLength, pattern, required } = component.validate as ValidateOptions
const errors: ValidationError[] = []

const c = (e: ValidationError) => errors.push(e)
const ctx = { ...component.validate, field: gettext(component.label, i18n).toLowerCase() }

const promises = [
await validateMaxLength(value, maxLength).catch((e) => errors.push(e)),
await validateMinLength(value, minLength).catch((e) => errors.push(e)),
await validatePattern(value, pattern).catch((e) => errors.push(e)),
await validateRequired(value, required).catch((e) => errors.push(e))
await validateMaxLength(value, maxLength as number, gettext('maxLength', i18n, ctx)).catch(c),
await validateMinLength(value, minLength as number, gettext('minLength', i18n, ctx)).catch(c),
await validatePattern(value, pattern as string, gettext('pattern', i18n, ctx)).catch(c),
await validateRequired(value, required as boolean, gettext('required', i18n, ctx)).catch(c)
]

return Promise.all(promises).then(() => {
Expand All @@ -41,21 +50,21 @@ export class ValidationError extends Error {
* Validates whether stringified value is at most maxLength characters.
* @param {boolean|number|string} value Value to check.
* @param {number|string} maxLength Maximum length.
* @param {string} message Message to show when invalid.
* @throws {MaxLengthValidationError} As promise rejection if invalid.
* @return {Promise<boolean>} Promise for true if valid.
*/
export const validateMaxLength = async (
value: boolean | number | string,
maxLength: number | string
maxLength: number | string,
message: string
): Promise<boolean> => {
const length = String(value).length
const limit = parseInt(maxLength as string)
const valid = Boolean(isNaN(limit) || length <= limit)

if (!valid) {
throw new MaxLengthValidationError(
`Ensure that this value has at most ${limit} characters (is has ${length}).`
)
throw new MaxLengthValidationError(message)
}
return valid
}
Expand All @@ -68,21 +77,21 @@ export class MaxLengthValidationError extends ValidationError {
* Validates whether stringified value is at least minLength characters.
* @param {boolean|number|string} value Value to check.
* @param {number|string} minLength Minimum length.
* @param {string} message Message to show when invalid.
* @throws {MinLengthValidationError} As promise rejection if invalid.
* @return {Promise<boolean>} Promise for true if valid.
*/
export const validateMinLength = async (
value: boolean | number | string,
minLength: number | string
minLength: number | string,
message: string
): Promise<boolean> => {
const length = String(value).length
const limit = parseInt(minLength as string)
const valid = Boolean(isNaN(limit) || length >= limit)

if (!valid) {
throw new MinLengthValidationError(
`Ensure that this value has at least ${limit} characters (is has ${length}).`
)
throw new MinLengthValidationError(message)
}
return valid
}
Expand All @@ -95,16 +104,18 @@ export class MinLengthValidationError extends ValidationError {
* Validates whether stringified value matches a (RegExp) pattern.
* @param {boolean|number|string} value Value to check.
* @param {RegExp|string} pattern Regular expression.
* @param {string} message Message to show when invalid.
* @throws {PatternValidationError} As promise rejection if invalid.
* @return {Promise<boolean>} Promise for true if valid.
*/
export const validatePattern = async (
value: boolean | number | string,
pattern: RegExp | string
pattern: RegExp | string,
message: string
): Promise<boolean> => {
const valid = Boolean(!pattern || String(value).match(pattern))
if (!valid) {
throw new PatternValidationError('Enter a valid value.')
throw new PatternValidationError(message)
}
return valid
}
Expand All @@ -117,16 +128,18 @@ export class PatternValidationError extends ValidationError {
* Validates whether value is truthy and required is true.
* @param {boolean|number|string} value Value to check.
* @param {boolean} required
* @param {string} message Message to show when invalid.
* @throws {RequiredValidationError} As promise rejection if invalid.
* @return {Promise<boolean>} Promise for true if valid.
*/
export const validateRequired = async (
value: boolean | number | string,
required: boolean
required: boolean,
message: string
): Promise<boolean> => {
const valid = Boolean(!required || value)
if (!valid) {
throw new RequiredValidationError('Field is required.')
throw new RequiredValidationError(message)
}
return valid
}
Expand Down
6 changes: 6 additions & 0 deletions src/types/config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export interface RenderConfiguration {
components: ComponentConfiguration[]

renderer: { renderTree: Function; renderBranch: Function; renderLeaf: Function }

i18n?: I18N
}

export interface CallbackConfiguration {
Expand All @@ -61,3 +63,7 @@ export interface FormErrors {
}

export type ComponentErrors = string[]

export interface I18N {
[index: string]: { [index: string]: string }
}

0 comments on commit befb83f

Please sign in to comment.