diff --git a/README.md b/README.md index 6c4058a..464509b 100644 --- a/README.md +++ b/README.md @@ -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 - `` 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 +``` diff --git a/index.ts b/index.ts index dc10c80..b5cde06 100644 --- a/index.ts +++ b/index.ts @@ -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 = {} @@ -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( 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 } diff --git a/test/basic.test.tsx b/test/basic.test.tsx index 07383fb..0717b0a 100644 --- a/test/basic.test.tsx +++ b/test/basic.test.tsx @@ -1,3 +1,5 @@ +/// + import React from 'react' import { test, expect, mock, spyOn } from 'bun:test' import { render } from '@testing-library/react' @@ -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 () => { @@ -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', @@ -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' }, @@ -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') +})