Skip to content

Commit

Permalink
Merge pull request #4 from yuo-app/rettend
Browse files Browse the repository at this point in the history
Rettend
  • Loading branch information
MAttila42 authored Aug 30, 2024
2 parents 14740a3 + c8d851c commit 99d9668
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 67 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# lin

![NPM Version](https://img.shields.io/npm/v/%40yuo-app%2Flin)
![JSR Version](https://img.shields.io/jsr/v/%40yuo/lin?color=yellow)

auto-i18n
15 changes: 15 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# TODO

- [ ] `translate` command
- [x] Base functionality: translates all locales
- [x] skip locale json files that are completely translated
- [ ] if there is an incomplete local json, then the gpt will add the missing parts (still put everything in the context window)
- [ ] Usage with `--manual [key]`: Prompts the user to enter the translation for each key in all the locales that are not yet translated. If a key is specified then all languages are prompted.
- [ ] `add` command (`lin add <key>`)
- [ ] Default usage: Running `lin add <key>` prompts the user to enter the translation for the key in the default language. It is then automatically translated to all the locales and saved in the locale json files.
- [ ] If a key already exists, show error and prompt to use `lin translate --manual [key]` instead.
- [ ] Usage with `--locale`: Prompts the user to enter the translation for the key in the specified locale.
- [ ] Usage with `--manual`: Prompts the user to enter the translation for the key for all the locales.
- [ ] `convert` command
- [ ] i18n loader
- [ ] support for other i18n integrations
1 change: 0 additions & 1 deletion lin.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { defineConfig } from './src'

export default defineConfig({
model: 'gpt-4o-mini',
})
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@yuo/lin",
"name": "@yuo-app/lin",
"type": "module",
"version": "0.0.0",
"packageManager": "bun@1.1.25",
Expand All @@ -26,7 +26,7 @@
"dev": "bun --bun unbuild --stub",
"lint": "eslint . --fix",
"test": "echo \"Error: write tests you little shit\" && exit 1",
"release": "bumpp && npm publish"
"release": "bumpp && npm publish --access public"
},
"peerDependencies": {
"typescript": "^5.5.4"
Expand Down
7 changes: 5 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { defineCommand, runMain } from 'citty'
import { defineCommand, runMain, showUsage } from 'citty'
import { description, name, version } from '../package.json'
import { console } from './utils'
import { commands } from './commands'
Expand All @@ -19,9 +19,12 @@ const main = defineCommand({
...commonArgs,
},
subCommands: commands,
run({ args }) {
run({ args, cmd, rawArgs }) {
if (args.version)
console.log(`${name} \`v${version}\``)

if (rawArgs.length === 0)
showUsage(cmd)
},
})

Expand Down
44 changes: 27 additions & 17 deletions src/commands/translate.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,60 @@
import 'dotenv/config'
import process from 'node:process'
import fs from 'node:fs/promises'
import path from 'node:path'
import { defineCommand } from 'citty'
import OpenAI from 'openai'
import { defineCommand } from 'citty'
import { loadI18nConfig } from '../i18n'
import { resolveConfig } from '../config'
import { console } from '../utils'
import { ICONS, console, r, shapeMatches } from '../utils'

export default defineCommand({
meta: {
name: 'translate',
description: 'translate locales',
},
args: {
force: {
alias: 'f',
type: 'boolean',
description: 'force to translate all locales',
default: false,
},
},
async run({ args }) {
const { config } = await resolveConfig(args)
const i18n = loadI18nConfig()
const openai = new OpenAI({ apiKey: process.env[config.env as string] })
const openai = new OpenAI({ apiKey: process.env[config.env] })

const defaultLocale = await fs.readFile(
path.join(i18n.directory, `${i18n.default}.json`),
{ encoding: 'utf8' },
)
const defaultLocale = await fs.readFile(r(`${i18n.default}.json`, i18n), { encoding: 'utf8' })

for (const locale of i18n.locales.filter(l => l !== i18n.default)) {
await console.loading(`Translate ${locale}`, async () => {
if (!args.force) {
const localeJson = await fs.readFile(r(`${locale}.json`, i18n), { encoding: 'utf8' })

if (shapeMatches(JSON.parse(defaultLocale), JSON.parse(localeJson))) {
console.log(ICONS.note, `Skipped: **${locale}**`)
continue
}
}

await console.loading(`Translate: **${locale}**`, async () => {
const completion = await openai.chat.completions.create({
messages: [
{
role: 'system',
content: `You are a simple api that translates locale jsons from ${i18n.default} to ${locale}. Your recieve just the input json and return just the translated json.`,
content: `You are a simple api that translates locale jsons from ${i18n.default} to ${locale}.
Your recieve just the input json and return just the translated json.`,
},
{
role: 'user',
content: defaultLocale,
},
],
temperature: 0,
model: config.model as OpenAI.ChatModel,
...config.options,
response_format: { type: 'json_object' },
})

await fs.writeFile(
path.join(i18n.directory, `${locale}.json`),
completion.choices[0].message.content as string,
{ encoding: 'utf8' },
)
await fs.writeFile(r(`${locale}.json`, i18n), completion.choices[0].message.content as string, { encoding: 'utf8' })
})
}
},
Expand Down
5 changes: 1 addition & 4 deletions src/commands/verify.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import process from 'node:process'
import { defineCommand } from 'citty'
import c from 'picocolors'
import { resolveConfig } from '../config'
import { console, createLoadingIndicator } from '../utils'
import { console } from '../utils'

export default defineCommand({
meta: {
Expand Down
96 changes: 68 additions & 28 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ import process from 'node:process'
import type OpenAI from 'openai'
import { loadConfig } from 'unconfig'
import { simpleMerge } from '@cross/deepmerge'
import type { ArgDef } from 'citty'
import type { ArgDef, StringArgDef } from 'citty'
import type { DeepRequired } from './utils'

type ChatModel = OpenAI.ChatModel
type OpenAIOptions = Partial<Pick<OpenAI.ChatCompletionCreateParamsNonStreaming, 'model'
| 'frequency_penalty'
| 'logit_bias'
| 'max_tokens'
| 'presence_penalty'
| 'seed'
| 'service_tier'
| 'temperature'
| 'top_p'>>

export const integrations = [
'i18n',
Expand All @@ -24,35 +34,41 @@ export interface Config {
* project root
* @default process.cwd()
*/
cwd?: string
cwd: string

/**
* the i18n integration used, by default `lin` will try to infer this
* @default undefined
*/
i18n?: Integration
i18n: Integration

/**
* OpenAI chat model to use
* @default gpt-4o-mini
* the environment variable that contains the OpenAI token.
* @default OPENAI_API_TOKEN
*/
model?: ChatModel
env: string

/**
* the environment variable that contains the OpenAI token.
* @default OPENAI_API_TOKEN
* the OpenAI options, like the model to use
*/
env?: string
options: OpenAIOptions
}

export const DEFAULT_CONFIG: Config = {
export const DEFAULT_CONFIG = {
i18n: 'i18n',
cwd: '',
model: 'gpt-4o-mini',
env: 'OPENAI_API_TOKEN',
}
options: {
model: 'gpt-4o-mini',
temperature: 0,
},
} satisfies Config

type Args = {
[key in keyof Config]-?: ArgDef
[key in keyof Config as key extends 'options' ? never : key]: ArgDef
} & {
model: ArgDef
temperature: ArgDef
}

export const commonArgs: Args = {
Expand All @@ -67,15 +83,23 @@ export const commonArgs: Args = {
type: 'string',
description: 'the i18n integration used',
},
env: {
alias: 'e',
type: 'string',
description: 'the environment variable that contains the OpenAI token',
default: DEFAULT_CONFIG.env,
},
model: {
alias: 'm',
type: 'string',
description: 'OpenAI chat model to use',
description: 'the model to use',
default: DEFAULT_CONFIG.options.model,
},
env: {
alias: 'e',
temperature: {
alias: 't',
type: 'string',
description: 'the environment variable that contains the OpenAI token',
description: 'the temperature to use',
default: DEFAULT_CONFIG.options.temperature.toString(),
},
}

Expand Down Expand Up @@ -111,27 +135,43 @@ function checkArg(name: string | undefined, list: readonly string[]) {
throw new Error(`"\`${name}\`" is invalid, must be one of ${list.join(', ')}`)
}

function normalizeArgs(args: Config): Partial<Config> {
const normalized: Partial<Config> = { ...args } as any
function normalizeArgs(args: Partial<Args>): Partial<Config> {
const normalized: Partial<Args> = { ...args }

Object.entries(commonArgs).forEach(([fullName, def]) => {
if ('alias' in def) {
if (def.alias && normalized[def.alias as keyof Config] !== undefined && normalized[fullName as keyof Config] === undefined) {
normalized[fullName as keyof Config] = normalized[def.alias as keyof Config] as any
delete normalized[def.alias as keyof Config]
const fullKey = fullName as keyof Args
const configKey = def.alias as keyof Args

if (def.alias && normalized[configKey] !== undefined && normalized[fullKey] === undefined) {
normalized[fullKey] = normalized[configKey]
delete normalized[configKey]
}
}
})

checkArg(normalized.i18n, integrations)
checkArg(normalized.model, models)
const config = convertType(normalized)
checkArg(config.i18n, integrations)
checkArg(config.options?.model, models)

return config
}

function convertType(config: any): Partial<Config> {
const { model, temperature, ...rest } = config

return normalized
return {
...rest,
options: {
model,
temperature,
},
}
}

export async function resolveConfig(
args: Record<string, any>,
): Promise<ReturnType<typeof loadConfig<Config>>> {
): Promise<ReturnType<typeof loadConfig<DeepRequired<Config>>>> {
const options = normalizeArgs(args)

const { config, sources, dependencies } = await loadConfig<Config>({
Expand Down Expand Up @@ -167,12 +207,12 @@ export async function resolveConfig(
})

return {
config: simpleMerge(config, options),
config: simpleMerge(config, options) as DeepRequired<Config>,
sources,
dependencies,
}
}

export function defineConfig(config: Partial<Config>): Config {
export function defineConfig(config: Partial<Config>): Partial<Config> {
return config
}
8 changes: 7 additions & 1 deletion src/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export function loadI18nConfig() {
export interface I18nConfig {
locales: string[]
default: string
directory: string
}

export function loadI18nConfig(): I18nConfig {
return {
locales: ['en-US', 'hu-HU', 'ko-KR'],
default: 'en-US',
Expand Down
Loading

0 comments on commit 99d9668

Please sign in to comment.