Skip to content

Commit

Permalink
feat(native): improve React Native support and allow setting language
Browse files Browse the repository at this point in the history
release-npm
  • Loading branch information
tobua committed Oct 21, 2023
1 parent daa7574 commit 6a33ed4
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 33 deletions.
62 changes: 54 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,77 @@

React translation library built with the Bun 🐰 runtime, Cursor AI 🖱️ editor and CodeWhisperer 🤫.

- Built for client-side Serverless architectures on the web
- Supports React Native
- 3 ways to store translations
- Translated with AI and cached during runtime in a Serverless function
- Generated with AI during build time and stored in a Serverless function
- Generated manually or with AI during build and stored in the bundle
- Support for React Native
- `<Text id="myTranslationKey" />` component to render translations
- Replacements with `{}` in translations
- Translations generated on demand using LLMs (AI)
- Optimized for development phase: no need to commit any translations to source!

## Usage
## Usage with Runtime Translation and Caching

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

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

translate('title') // My Title
translate('counter', '5') // Counter: 5
```

## Usage with Buildtime Translation

```ts
// TODO
```

## Usage with React Native

```tsx
import { create, Language } from 'epic-language/native'

const { translate, Text } = create(
{
title: 'My App',
},
{
[Language.es]: { title: 'My App' },
[Language.zh]: { title: 'My App' },
},
Language.en,
)

const settingsText = translate('title')
const Heading = <Text style={{ fontSize: 32 }}>title</Text>
```

For compatibility reasons the following `package.json` entries are required. The first entry adds package exports support to metro while the second downgrades [`chalk`](https://npmjs.com/chalk) to a version that does not yet require package imports.

```json
{
"metro": {
"resolver": {
"unstable_enablePackageExports": true
}
},
"overrides": {
"overrides": {
"chalk": "^4.1.2"
}
}
}
```
14 changes: 7 additions & 7 deletions helper.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { create } from 'logua'
import { Language } from './types'
import { Language, Languages } from './types'

export const log = create('epic-language', 'magenta')

// TODO for validation and country specific locales.
// Intl.getCanonicalLocales("EN-US"); // ["en-US"]

export const readableLanguage = {
export const readableLanguage: Record<Languages, string> = {
[Language.en]: 'English',
[Language.es]: 'Spanish',
[Language.zh]: 'Chinese',
[Language.de]: 'German',
[Language.fr]: 'French',
[Language.it]: 'Italian',
[Language.es]: 'Idioma',
[Language.zh]: '语言',
[Language.de]: 'Deutsch',
[Language.fr]: 'Français',
[Language.it]: 'Italiano',
}
24 changes: 16 additions & 8 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { useRef, useEffect, createElement } from 'react'
import { type Text as NativeText } from 'react-native'
import { log } from './helper'
import { log, readableLanguage } from './helper'
import { Sheets, Sheet, Language, Replacement, TextProps } from './types'
import { insertReplacements, replaceBracketsWithChildren } from './replace'

export { Language }
export { Language, readableLanguage }

const has = (object: object, key: string | number | symbol) => Object.hasOwn(object, key)

function getLanguage(defaultLanguage: Language) {
function getBrowserLanguage(defaultLanguage: Language) {
// @ts-ignore dom lib is added...
const language = (global.mockLanguage ?? window.navigator.language).substring(0, 2)
if (Object.values(Language).includes(language as Language)) return language
const locale = global.mockLanguage ?? window.navigator?.language ?? defaultLanguage
const language = locale.substring(0, 2) as Language
if (Object.values(Language).includes(language)) return language
return defaultLanguage
}

Expand Down Expand Up @@ -43,6 +44,7 @@ export function create<T extends Sheet>({
sheets = {},
languages = Object.keys(Language),
Type = 'span',
getLanguage = getBrowserLanguage,
}: {
translations: T
route?: string
Expand All @@ -51,8 +53,9 @@ export function create<T extends Sheet>({
sheets?: Sheets<T>
languages?: string[]
Type?: 'span' | 'p' | 'div' | 'a' | 'button' | typeof NativeText
getLanguage?: (language: Language) => Language
}) {
const userLanguage = getLanguage(defaultLanguage)
let userLanguage = getLanguage(defaultLanguage)
sheets[defaultLanguage] = translations

loadSheet(userLanguage, sheets, route, onLoad, defaultLanguage)
Expand Down Expand Up @@ -98,13 +101,18 @@ export function create<T extends Sheet>({
)

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

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

return { translate, Text, language: userLanguage }
function setLanguage(language: Language) {
userLanguage = language
loadSheet(language, sheets, route, onLoad, defaultLanguage)
}

return { translate, Text, language: userLanguage, setLanguage }
}
35 changes: 30 additions & 5 deletions native.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
import { Text } from 'react-native'
import { create as baseCreate } from './index'
import { Sheet, Sheets, Language } from './types'
import { Text, Platform, NativeModules } from 'react-native'
import { create as baseCreate, readableLanguage, Language } from 'epic-language'
import { Sheet, Sheets } from './types'

export { Language }
export { Language, readableLanguage }

function getNativeLanguage(defaultLanguage: Language) {
// https://stackoverflow.com/questions/33468746/whats-the-best-way-to-get-device-locale-in-react-native-ios/42655021
let locale =
Platform.OS === 'android'
? NativeModules.I18nManager.localeIdentifier
: NativeModules.SettingsManager.settings.AppleLocale

if (!locale) {
;[locale] = NativeModules.SettingsManager.settings.AppleLanguages
if (locale === undefined) {
locale = defaultLanguage
}
} else {
locale = locale.replace('_', '-')
}

return locale.substring(0, 2) as Language
}

export function create<T extends Sheet>(
translations: T,
sheets: Sheets<T>,
defaultLanguage: Language = Language.en,
) {
return baseCreate({ translations, defaultLanguage, sheets, Type: Text })
return baseCreate({
translations,
defaultLanguage,
sheets,
Type: Text,
getLanguage: getNativeLanguage,
})
}
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
"index.ts",
"native.ts"
],
"esbuild": {
"external": [
"epic-language"
]
},
"tsconfig": {
"compilerOptions": {
"types": [
Expand All @@ -25,6 +30,9 @@
"paths": {
"react-native": [
"./test/react-native.mock"
],
"epic-language": [
"./index"
]
}
}
Expand Down
7 changes: 3 additions & 4 deletions test/ai.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ test('Translates several to various languages.', async () => {
description: '这是描述。',
})

expect(sheetGerman).toEqual({
title: 'Mein Titel',
description: 'Dies ist die Beschreibung.',
})
expect(sheetGerman.title).toBe('Mein Titel')
// Das / Dies.
expect(sheetGerman.description).toContain('ist die Beschreibung.')
}, 20000)
14 changes: 14 additions & 0 deletions test/basic.browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,17 @@ test('Available languages can be restricted.', () => {
// Language is listed.
expect(translate('title', undefined, Language.zh)).toBe(chineseSheet.title)
})

test('Language can be changed after initialization.', () => {
const { translate, setLanguage } = create({
translations: englishSheet,
sheets: {
[Language.en]: englishSheet,
[Language.zh]: chineseSheet,
},
})

expect(translate('description')).toBe(englishSheet.description)
setLanguage(Language.zh)
expect(translate('title')).toBe(chineseSheet.title)
})
16 changes: 16 additions & 0 deletions test/react-native.mock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,19 @@ import React from 'react'
export function Text(props: any) {
return <p {...props} />
}

export const Platform = {
OS: 'android',
}

export const NativeModules = {
I18nManager: {
localeIdentifier: 'en_US',
},
SettingsManager: {
settings: {
AppleLocale: 'en_US',
AppleLanguages: ['en_US'],
},
},
}
2 changes: 1 addition & 1 deletion translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const openai = new OpenAI({
})

export async function translate(input: string, language: Language) {
const prompt = `Translate the following object values to ${readableLanguage[language]} keeping the object keys intact:
const prompt = `Translate the following object values to ${readableLanguage[language]}. Please do not translate the object keys.
${input}`

Expand Down
2 changes: 2 additions & 0 deletions types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export enum Language {
it = 'it',
}

export type Languages = keyof typeof Language

export type TextProps = DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement> &
DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement> &
DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>

0 comments on commit 6a33ed4

Please sign in to comment.