Skip to content

Commit

Permalink
feat(translations): implement replacements and trigger initial release
Browse files Browse the repository at this point in the history
release-npm
  • Loading branch information
tobua committed Oct 16, 2023
1 parent 55ed34d commit 02ff2a2
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 6 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,26 @@ React translation library built with the Bun 🐰 runtime and Cursor AI 🖱️
- Built for client-side Serverless architectures on the web
- Supports React Native
- `<Text id="myTranslationKey" />` component to render translations
- Replacements in translations: {}
- Replacements with `{}` in translations

## Usage

```ts
import { translations } from 'epic-language'

const { translate } = translations(
// Initial translations in default language.
{
title: 'My Title',
description: 'This is the description.',
counter: 'Count: {}',
},
// Route to load translations for user language.
'/api/translations',
// Callback for when translations have been loaded.
onLoad,
)

translate('title') // My Title
translate('counter', '5') // Counter: 5
```
26 changes: 23 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { log } from './helper'

type Sheet = { [key: string]: string }
type Sheets = { [key in Language]?: Sheet }
type Replacement = string | number

const sheets: Sheets = {}

Expand Down Expand Up @@ -40,25 +41,44 @@ async function loadSheet(apiRoute: string, onLoad: () => void, defaultLanguage:
})
}

function insertReplacements(translation: string, replacements?: Replacement | Replacement[]) {
if (!replacements) return translation

if (!Array.isArray(replacements)) {
// eslint-disable-next-line no-param-reassign
replacements = [replacements]
}

let result = translation
for (let index = 0; index < replacements.length; index += 1) {
result = result.replace('{}', String(replacements[index]))
}
return result
}

// defaultLanguage is the language in the standard translation that's always loaded.
export function translations<T extends Sheet>(
defaultTranslations: T,
apiRoute: string,
onLoad: () => void,
onLoad: () => void = () => {},
defaultLanguage: Language = Language.en,
) {
sheets[defaultLanguage] = defaultTranslations

loadSheet(apiRoute, onLoad, defaultLanguage)

function translate(key: keyof T, language: Language = defaultLanguage) {
function translate(
key: keyof T,
replacements?: Replacement | Replacement[],
language: Language = defaultLanguage,
) {
const sheet = sheets[language]
if (!has(sheet, key)) {
log(`Translation for key "${String(key)}" is missing`)
if (Object.hasOwn(sheets[defaultLanguage], key)) return sheets[defaultLanguage][key as string]
return key
}
return sheet[key as string]
return insertReplacements(sheet[key as string], replacements)
}

return { translate }
Expand Down
14 changes: 12 additions & 2 deletions test/basic.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/// <reference lib="dom" />

import React from 'react'
import { test, expect, mock, spyOn } from 'bun:test'
import { render } from '@testing-library/react'
Expand All @@ -9,6 +11,7 @@ GlobalRegistrator.register()
console.log = log // Restore log to show up in tests during development.

const windowLanguageSpy = spyOn(window, 'navigator')
// @ts-ignore
windowLanguageSpy.mockImplementation(() => ({ language: 'en_US' }))

test('Can render a basic app.', async () => {
Expand All @@ -32,7 +35,7 @@ test('Can render a basic app.', async () => {
})

test('Translates key in initially provided language.', () => {
const onLoad = mock()
const onLoad = mock(() => {})
const { translate } = translations(
{ title: 'My Title', description: 'This is the description.' },
'/api/translations',
Expand All @@ -49,7 +52,7 @@ test('Translates key in initially provided language.', () => {

test('Symbols or numbers cannot be used as keys.', () => {
// TODO doesn't seem possible to restrict keys to string when using generic type with extends.
const onLoad = mock()
const onLoad = mock(() => {})
const symbol = Symbol('test')
const { translate } = translations(
{ [symbol]: 'My Symbol', 5: 'My Number' },
Expand All @@ -64,3 +67,10 @@ test('Symbols or numbers cannot be used as keys.', () => {
expect(translate('missing')).toBe('missing')
expect(onLoad).toHaveBeenCalled()
})

test('Replacements are inserted.', () => {
const { translate } = translations({ counter: 'Count: {}' }, '/api/translations')

expect(translate('counter', '123')).toBe('Count: 123')
expect(translate('counter', 456)).toBe('Count: 456')
})

0 comments on commit 02ff2a2

Please sign in to comment.