Skip to content

Commit

Permalink
feat(translate): implement and tests generating translations with LLMs
Browse files Browse the repository at this point in the history
  • Loading branch information
tobua committed Oct 18, 2023
1 parent b296d1d commit 4c210f9
Show file tree
Hide file tree
Showing 9 changed files with 93 additions and 9 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,13 @@ jobs:
- run: bun install
- name: 🚧 Build
run: bun run build
- name: 🧪 Test
run: bun test
- name: 🧪 Test Browser
run: bun test -- basic react
# Tests running in parallel and browser environment cannot be torn down properly after tests.
- name: 🧪 Test Server
run: bun test -- interface ai
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- run: npm install -g npm@latest
- name: 📢 Release
uses: tobua/release-npm-action@v2
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ package-lock.json
tsconfig.json
dist
bun.lockb
.env
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ React translation library built with the Bun 🐰 runtime and Cursor AI 🖱️
- Supports 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

Expand Down
14 changes: 12 additions & 2 deletions helper.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
import { create } from "logua";
import { create } from 'logua'
import { Language } from './types'

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

export const readableLanguage = {
[Language.en]: 'English',
[Language.es]: 'Spanish',
[Language.zh]: 'Chinese',
[Language.de]: 'German',
[Language.fr]: 'French',
[Language.it]: 'Italian',
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,18 @@
"devDependencies": {
"@happy-dom/global-registrator": "^12.9.1",
"@testing-library/react": "^14.0.0",
"@types/react": "^18.2.28",
"@types/react-dom": "^18.2.13",
"@types/react": "^18.2.29",
"@types/react-dom": "^18.2.14",
"bun-types": "^1.0.6",
"happy-dom": "^12.9.1",
"msw": "^1.3.2",
"openai": "^4.12.4",
"padua": "^2.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"peerDependencies": {
"openai": ">= 4",
"react": ">= 18",
"react-native": ">= 72"
},
Expand Down
38 changes: 38 additions & 0 deletions test/ai.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { test, expect } from 'bun:test'
import { translate } from '../translate'
import { Language } from '../types'

// Bun automatically reads env variables.

test('Translates single key to various languages.', async () => {
const inputSheet = JSON.stringify({ title: 'My Title' })
const sheetSpanish = await translate(inputSheet, Language.es)
const sheetChinese = await translate(inputSheet, Language.zh)
const sheetGerman = await translate(inputSheet, Language.de)

// Casing can vary in Spanish.
expect(sheetSpanish.title.toLowerCase()).toBe('Mi Título'.toLowerCase())
expect(sheetChinese.title).toBe('我的标题')
expect(sheetGerman.title).toBe('Mein Titel')
})

test('Translates several to various languages.', async () => {
const inputSheet = JSON.stringify({ title: 'My Title', description: 'This is the description.' })
const sheetFrench = await translate(inputSheet, Language.fr)
const sheetChinese = await translate(inputSheet, Language.zh)
const sheetGerman = await translate(inputSheet, Language.de)

expect(sheetFrench.title).toBe('Mon titre')
// Ceci est la description. / Voici la description.
expect(sheetFrench.description).toContain('la description')

expect(sheetChinese).toEqual({
title: '我的标题',
description: '这是描述。',
})

expect(sheetGerman).toEqual({
title: 'Mein Titel',
description: 'Dies ist die Beschreibung.',
})
}, 20000)
2 changes: 0 additions & 2 deletions test/interface.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/// <reference lib="dom" />

import { test, expect, mock, beforeAll, afterAll } from 'bun:test'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
Expand Down
28 changes: 28 additions & 0 deletions translate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import OpenAI from 'openai'
import type { Language } from './types'
import { readableLanguage } from './helper'

const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
organization: 'org-jec41Owv49cySXoL8adosmOh',
})

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

const chatCompletion = await openai.chat.completions.create({
model: 'gpt-3.5-turbo',
stream: false,
messages: [{ role: 'user', content: prompt }],
})

const { choices } = chatCompletion

if (choices.length === 1) {
return JSON.parse(choices[0].message.content ?? '{}')
}

return {}
}
2 changes: 1 addition & 1 deletion types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export type Replacement = string | number
export enum Language {
en = 'en',
es = 'es',
cn = 'cn',
zh = 'zh',
de = 'de',
fr = 'fr',
it = 'it',
Expand Down

0 comments on commit 4c210f9

Please sign in to comment.