Skip to content

Commit

Permalink
feat(text): ordered replacements and JSX replacements
Browse files Browse the repository at this point in the history
  • Loading branch information
tobua committed Jan 31, 2024
1 parent 377c430 commit 0a45c31
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 24 deletions.
16 changes: 10 additions & 6 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useRef, useEffect, createElement } from 'react'
import { useRef, useEffect, createElement, type ReactNode } from 'react'
import { type Text as NativeText } from 'react-native'
import { log, readableLanguage } from './helper'
import { Sheets, Sheet, Language, Replacement, TextProps } from './types'
Expand Down Expand Up @@ -116,17 +116,21 @@ export function create<T extends Sheet>({
const defaultSheet = (sheets[defaultLanguage] ?? {}) as Sheet<keyof T>
const translation = sheet[key] ?? defaultSheet[key] ?? String(key)
const Component = as
const filledContent = replaceBracketsWithChildren(
translation,
Array.isArray(children) ? children : [children],
)
const possibleReplacements = id && !replacements ? children : replacements
const arrayReplacements = Array.isArray(possibleReplacements)
? possibleReplacements
: [possibleReplacements]
let filledContent: ReactNode = translation

if (arrayReplacements.length > 0) {
filledContent = replaceBracketsWithChildren(translation, arrayReplacements)
}

useEffect(() => {
// TODO Native replacement onLoad.
// console.log('effect', ref.current)
}, [])

// @ts-ignore Issues with ref.
return createElement(Component, { ...props, ref }, filledContent)
}

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@
},
"devDependencies": {
"@elysiajs/cors": "^0.8.0",
"@happy-dom/global-registrator": "^13.3.1",
"@testing-library/react": "^14.1.2",
"@happy-dom/global-registrator": "^13.3.8",
"@testing-library/react": "^14.2.0",
"@types/bun": "^1.0.4",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"avait": "^0.7.0",
"elysia": "^0.8.14",
"happy-dom": "^13.3.1",
"elysia": "^0.8.15",
"happy-dom": "^13.3.8",
"openai": "^4.26.0",
"padua": "^2.0.9",
"react": "^18.2.0",
Expand Down
43 changes: 30 additions & 13 deletions replace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,22 +24,39 @@ export function insertReplacements(
return result
}

export function replaceBracketsWithChildren(text: string, replacements?: ReactNode[]) {
if (!replacements || !text.includes('{}')) return text
// Other than JSX.Element only string and number are allowed and we check them here.
const isNode = (value: unknown) => typeof value !== 'number' && typeof value !== 'string'

const parts = text.split('{}')
// TODO ReactNode or JSX.Element?
export function replaceBracketsWithChildren(text: string, replacements: Replacement[]) {
const parts = text.match(/({\d+})|({})|([^{}]+)/g)
const result: ReactNode[] = []
let currentIndex = 0

for (let index = 0; index < parts.length; index += 1) {
result.push(parts[index])
if (index < parts.length - 1) {
currentIndex %= replacements.length
// @ts-ignore
result.push(cloneElement(replacements[currentIndex], { key: currentIndex }))
currentIndex += 1

if (!parts) return [text]

parts.forEach((part, partIndex) => {
if (part.startsWith('{') && part.endsWith('}')) {
const index = parseInt(part.slice(1, -1), 10)
if (!Number.isNaN(index) && index <= replacements.length) {
const replacement = replacements[index - 1]
if (!isNode(replacement)) {
result.push(replacement)
} else {
// TODO clone necessary?
result.push(cloneElement(replacements[index - 1] as JSX.Element, { key: index }))
}
} else {
const replacement = replacements.shift()
result.push(
isNode(replacement)
? cloneElement(replacement as JSX.Element, { key: partIndex })
: replacement,
)
}
} else {
result.push(part)
}
}
})

return result
}
14 changes: 14 additions & 0 deletions test/ai.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,17 @@ test('Translates several to various languages.', async () => {
// Das / Dies.
expect(sheetGerman.description).toContain('ist die Beschreibung.')
}, 20000)

test('Replacement position stays intact.', async () => {
const inputSheet = JSON.stringify({
regular: 'first {} second {} third',
ordered: 'one {2} two {1} three',
})
const sheetGerman = await translate(inputSheet, Language.de)

expect(
sheetGerman.regular === 'erstes {} zweites {} drittes' ||
sheetGerman.regular === 'erste {} zweite {} dritte',
).toBe(true)
expect(sheetGerman.ordered).toBe('eins {2} zwei {1} drei')
}, 20000)
37 changes: 37 additions & 0 deletions test/react.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ beforeEach(() => {
globalThis.mockLanguage = 'en_US'
})

const serializeDocument = (node: Element = document.body) => {
const serializer = new XMLSerializer()
return serializer.serializeToString(node)
}

test('Text component can be used to render translations.', () => {
const onLoad = mock(() => {})
const { Text } = create({
Expand Down Expand Up @@ -38,3 +43,35 @@ test('Text component can be used to render translations.', () => {

app.unmount()
})

test('Text and JSX can be used as replacements.', () => {
const onLoad = mock(() => {})
const { Text } = create({
translations: {
regular: 'first {} second {} third',
ordered: 'one {2} two {1} three',
},
route: '/api/translations',
onLoad,
defaultLanguage: Language.en,
})

const app = render(
<div>
<Text id="regular" replacements={['123', '456']} />
<Text replacements={['123', '456']}>ordered</Text>
<Text id="regular" replacements={[<span>123</span>, <p>456</p>]} />
<Text replacements={[<p>123</p>, <span>456</span>]}>ordered</Text>
</div>,
)

const serialized = serializeDocument()

expect(onLoad).toHaveBeenCalled()
expect(serialized).toContain('first 123 second 456 third')
expect(serialized).toContain('one 456 two 123 three')
expect(serialized).toContain('first <span>123</span> second <p>456</p> third')
expect(serialized).toContain('one <span>456</span> two <p>123</p> three')

app.unmount()
})
37 changes: 37 additions & 0 deletions test/unit.node.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from 'react'
import { test, expect } from 'bun:test'
import { replaceBracketsWithChildren } from '../replace'

test('Replaces brackets with text or JSX.', () => {
expect(replaceBracketsWithChildren('one {} two', ['123']).join('')).toBe('one 123 two')
expect(replaceBracketsWithChildren('one {} two {} three', ['123', '456']).join('')).toBe(
'one 123 two 456 three',
)

const jsxResultUnordered = replaceBracketsWithChildren('one {} two', [<p>123</p>])
expect(jsxResultUnordered[0]).toBe('one ')
expect(jsxResultUnordered[1]).toEqual(<p key="1">123</p>)
expect(jsxResultUnordered[2]).toBe(' two')

const jsxResultUnorderedMultiple = replaceBracketsWithChildren('one {} two {} three', [
<p>123</p>,
<span>456</span>,
])
expect(jsxResultUnorderedMultiple[4]).toBe(' three')
expect(jsxResultUnorderedMultiple[3]).toEqual(<span key="3">456</span>)
expect(jsxResultUnorderedMultiple[2]).toBe(' two ')
})

test('Replaces ordered brackets with text or JSX.', () => {
expect(replaceBracketsWithChildren('one {2} two {1} three', ['123', '456']).join('')).toBe(
'one 456 two 123 three',
)

const jsxResultOrderedMultiple = replaceBracketsWithChildren('one {2} two {1} three', [
<p>123</p>,
<span>456</span>,
])
expect(jsxResultOrderedMultiple[2]).toBe(' two ')
expect(jsxResultOrderedMultiple[1]).toEqual(<span key="2">456</span>)
expect(jsxResultOrderedMultiple[3]).toEqual(<p key="1">123</p>)
})
2 changes: 1 addition & 1 deletion types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type Sheet<T extends string | number | symbol = string> = {
[key in T]: string
}
export type Sheets<T extends Sheet> = { [key in Language]?: Sheet<keyof T> }
export type Replacement = string | number
export type Replacement = string | number | JSX.Element

// https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
export enum Language {
Expand Down

0 comments on commit 0a45c31

Please sign in to comment.