@drone1/alt 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/.nvmrc +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +247 -0
  4. package/assets/figlet-fonts/THIS.flf +720 -0
  5. package/bin.mjs +8 -0
  6. package/localization/.localization.cache.json +1872 -0
  7. package/localization/aa.json +24 -0
  8. package/localization/af.json +24 -0
  9. package/localization/agq.json +24 -0
  10. package/localization/ak.json +24 -0
  11. package/localization/am.json +24 -0
  12. package/localization/ar.json +24 -0
  13. package/localization/as.json +24 -0
  14. package/localization/asa.json +24 -0
  15. package/localization/ast.json +24 -0
  16. package/localization/az.json +24 -0
  17. package/localization/ba.json +24 -0
  18. package/localization/bas.json +24 -0
  19. package/localization/be.json +24 -0
  20. package/localization/bem.json +24 -0
  21. package/localization/bez.json +24 -0
  22. package/localization/bg.json +24 -0
  23. package/localization/bm.json +24 -0
  24. package/localization/bn.json +24 -0
  25. package/localization/bo.json +24 -0
  26. package/localization/br.json +24 -0
  27. package/localization/brx.json +24 -0
  28. package/localization/bs.json +24 -0
  29. package/localization/byn.json +24 -0
  30. package/localization/ca.json +24 -0
  31. package/localization/ccp.json +24 -0
  32. package/localization/cd-RU.json +24 -0
  33. package/localization/ceb.json +24 -0
  34. package/localization/cgg.json +24 -0
  35. package/localization/chr.json +24 -0
  36. package/localization/co.json +24 -0
  37. package/localization/config.json +76 -0
  38. package/localization/cs.json +24 -0
  39. package/localization/cu-RU.json +24 -0
  40. package/localization/da.json +24 -0
  41. package/localization/de-AT.json +24 -0
  42. package/localization/de-CH.json +24 -0
  43. package/localization/de-DE.json +24 -0
  44. package/localization/dua.json +24 -0
  45. package/localization/dv.json +24 -0
  46. package/localization/dz.json +24 -0
  47. package/localization/ebu.json +24 -0
  48. package/localization/en.json +26 -0
  49. package/localization/es-ES.json +24 -0
  50. package/localization/es-MX.json +24 -0
  51. package/localization/et.json +24 -0
  52. package/localization/eu.json +24 -0
  53. package/localization/fr-CA.json +24 -0
  54. package/localization/fr-CH.json +24 -0
  55. package/localization/fr-FR.json +24 -0
  56. package/localization/gsw.json +24 -0
  57. package/localization/hi.json +24 -0
  58. package/localization/hr.json +24 -0
  59. package/localization/hy.json +24 -0
  60. package/localization/ja.json +24 -0
  61. package/localization/km.json +24 -0
  62. package/localization/ksf.json +24 -0
  63. package/localization/ku.json +24 -0
  64. package/localization/kw.json +24 -0
  65. package/localization/my.json +24 -0
  66. package/localization/nl.json +24 -0
  67. package/localization/prs.json +24 -0
  68. package/localization/reference.js +21 -0
  69. package/localization/ru.json +24 -0
  70. package/localization/sq.json +24 -0
  71. package/localization/swc.json +24 -0
  72. package/localization/th.json +24 -0
  73. package/localization/tzm-Latn-.json +24 -0
  74. package/localization/uk.json +24 -0
  75. package/localization/vi.json +24 -0
  76. package/localization/zh-Hans.json +24 -0
  77. package/localization/zh-Hant.json +24 -0
  78. package/npm-shrinkwrap.json +1143 -0
  79. package/package.json +48 -0
  80. package/src/assert.js +19 -0
  81. package/src/cache.js +12 -0
  82. package/src/config.js +26 -0
  83. package/src/consts.js +23 -0
  84. package/src/context-keys.js +10 -0
  85. package/src/io.js +129 -0
  86. package/src/localizer/localize.js +135 -0
  87. package/src/logging.js +35 -0
  88. package/src/logo.js +22 -0
  89. package/src/main.mjs +103 -0
  90. package/src/options.js +18 -0
  91. package/src/provider.js +17 -0
  92. package/src/providers/anthropic.mjs +39 -0
  93. package/src/providers/openai.mjs +30 -0
  94. package/src/shutdown.js +36 -0
  95. package/src/translate.js +602 -0
  96. package/src/utils.js +108 -0
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@drone1/alt",
4
+ "version": "0.4.0",
5
+ "description": "An AI-powered localization tool",
6
+ "main": "src/index.mjs",
7
+ "bin": {
8
+ "alt": "./bin.mjs"
9
+ },
10
+ "scripts": {
11
+ "localize-display-strings": "node ./bin.mjs --reference-file localization/reference.js "
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/drone1/ai-localization-tool.git"
16
+ },
17
+ "keywords": [
18
+ "localization",
19
+ "i18n",
20
+ "translation",
21
+ "cli",
22
+ "ai",
23
+ "claude",
24
+ "openai",
25
+ "gemini",
26
+ "google",
27
+ "anthropic"
28
+ ],
29
+ "author": "drone1",
30
+ "license": "MIT",
31
+ "bugs": {
32
+ "url": "https://github.com/drone1/ai-localization-tool/issues"
33
+ },
34
+ "homepage": "https://github.com/drone1/ai-localization-tool#readme",
35
+ "dependencies": {
36
+ "axios": "^1.8.4",
37
+ "chalk": "^5.4.1",
38
+ "commander": "^11.1.0",
39
+ "figlet": "^1.8.0",
40
+ "gradient-string": "^3.0.0",
41
+ "listr2": "^8.2.5",
42
+ "locale-codes": "^1.3.1",
43
+ "strip-json-comments": "^5.0.1"
44
+ },
45
+ "engines": {
46
+ "node": ">=14.0.0"
47
+ }
48
+ }
package/src/assert.js ADDED
@@ -0,0 +1,19 @@
1
+ export function assert(b, msg) {
2
+ if (!b) {
3
+ debugger
4
+ throw new Error(msg || `Assertion failed`)
5
+ }
6
+ }
7
+
8
+ export function assertIsNonEmptyString(s, msg) {
9
+ assert(s?.length, msg || `parameter was not a non-empty string`)
10
+ }
11
+
12
+ export function assertValidPath(path, msg) {
13
+ assertIsNonEmptyString(path, msg || `parameter was not a valid path`)
14
+ }
15
+
16
+ export function assertIsObj(x, msg) {
17
+ assert(typeof x === 'object', msg || `parameter was not an object`)
18
+ }
19
+
package/src/cache.js ADDED
@@ -0,0 +1,12 @@
1
+ import { readJsonFile } from './io.js'
2
+
3
+ export async function loadCache(path) {
4
+ const storedCache = await readJsonFile(path)
5
+ return {
6
+ referenceHash: storedCache?.referenceHash ?? '',
7
+ referenceKeyHashes: storedCache?.referenceKeyHashes ?? {},
8
+ state: storedCache?.state ?? {},
9
+ lastRun: storedCache?.lastRun ?? null,
10
+ }
11
+ }
12
+
package/src/config.js ADDED
@@ -0,0 +1,26 @@
1
+ import * as path from 'path'
2
+ import { DEFAULT_CONFIG_FILENAME } from './consts.js'
3
+ import { readJsonFile } from './io.js'
4
+
5
+ export async function loadConfig({ configFile, refFileDir, log }) {
6
+ let configFilePath
7
+ if (configFile?.length) {
8
+ log.V(`Using config file specified by --config-file "${configFile}"...`)
9
+ configFilePath = configFile
10
+ } else {
11
+ log.V(`Using config file path based on reference file dir, "${refFileDir}"...`)
12
+ configFilePath = path.resolve(refFileDir, DEFAULT_CONFIG_FILENAME)
13
+ }
14
+
15
+ log.V(`Attempting to load config file from "${configFilePath}"`)
16
+ return await readJsonFile(configFilePath) || {
17
+ provider: null,
18
+ targetLanguages: [],
19
+ lookForContextData: true,
20
+ contextPrefix: '',
21
+ contextSuffix: '',
22
+ referenceLanguage: null,
23
+ normalizeOutputFilenames: false
24
+ }
25
+ }
26
+
package/src/consts.js ADDED
@@ -0,0 +1,23 @@
1
+ import * as path from 'path'
2
+
3
+ export const TRANSLATION_FAILED_RESPONSE_TEXT = '<<<TRANSLATION_FAILED>>>'
4
+ export const LANGTAG_ENGLISH = 'en'
5
+ export const LANGTAG_DEFAULT = LANGTAG_ENGLISH
6
+
7
+ export const VALID_TRANSLATION_PROVIDERS = [
8
+ 'anthropic',
9
+ 'openai'
10
+ ]
11
+
12
+ export const ENV_VARS = [
13
+ { name: 'ANTHROPIC_API_KEY', description: 'Your Anthropic API key' },
14
+ { name: 'OPENAI_API_KEY', description: 'Your OpenAI API key' },
15
+ { name: 'ALT_LANGUAGE', description: 'POSIX locale used for display' }
16
+ ]
17
+
18
+ export const LOCALIZATION_SRC_DIR = path.resolve('localization')
19
+ export const DEFAULT_CACHE_FILENAME = '.localization.cache.json'
20
+ export const DEFAULT_CONFIG_FILENAME = 'config.json'
21
+ export const OVERLOADED_BACKOFF_INTERVAL_MS = 30 * 1000
22
+ export const CWD = process.cwd()
23
+
@@ -0,0 +1,10 @@
1
+ export function isContextKey({ key, contextPrefix, contextSuffix }) {
2
+ if (contextPrefix?.length) return key.startsWith(contextPrefix)
3
+ if (contextSuffix?.length) return key.endsWith(contextSuffix)
4
+ throw new Error(`Either the context prefix or context suffix must be defined`)
5
+ }
6
+
7
+ export function formatContextKeyFromKey({ key, prefix, suffix }) {
8
+ return `${prefix}${key}${suffix}`
9
+ }
10
+
package/src/io.js ADDED
@@ -0,0 +1,129 @@
1
+ import * as os from 'os'
2
+ import * as fs from 'fs'
3
+ import * as fsp from 'fs/promises'
4
+ import * as path from 'path'
5
+ import stripJsonComments from 'strip-json-comments'
6
+ import { ensureExtension, normalizeKey } from './utils.js'
7
+ import { assertIsObj, assertValidPath } from './assert.js'
8
+ import { pathToFileURL } from 'url'
9
+ import { CWD } from './consts.js'
10
+
11
+ export async function mkTmpDir() {
12
+ return await fsp.mkdtemp(path.join(os.tmpdir(), 'alt-'))
13
+ }
14
+
15
+ export function parseJson(s) {
16
+ try {
17
+ return JSON.parse(s)
18
+ } catch (e) {
19
+ return null
20
+ }
21
+ }
22
+
23
+ export async function readFileAsText(filePath) {
24
+ try {
25
+ return await fsp.readFile(filePath, 'utf8')
26
+ } catch (error) {
27
+ if (error.code === 'ENOENT') {
28
+ return null
29
+ }
30
+ throw error
31
+ }
32
+ }
33
+
34
+ export async function readJsonFile(filePath, isJSONComments = false) {
35
+ let content = await readFileAsText(filePath)
36
+ if (isJSONComments) content = stripJsonComments.stripJsonComments(content)
37
+ return parseJson(content)
38
+ }
39
+
40
+ export async function fileExists(path) {
41
+ try {
42
+ await fsp.access(path)
43
+ return true
44
+ } catch {
45
+ return false
46
+ }
47
+ }
48
+
49
+ export function rmDir(dir, log) {
50
+ try {
51
+ fs.rmSync(dir, { recursive: true, force: true })
52
+ log.D(`Removed dir ${dir}`)
53
+ } catch (error) {
54
+ log.E(`Error cleaning up temp directory "${dir}": ${error.message}`)
55
+ throw error
56
+ }
57
+ }
58
+
59
+ // This is basically so that we can dynamically import .js files by copying them to temp .mjs files, to avoid errors from node
60
+ export async function copyFileToTempAndEnsureExtension({ filePath, tmpDir, ext }) {
61
+ try {
62
+ const fileName = ensureExtension(path.basename(filePath), ext)
63
+ const destPath = path.join(tmpDir, fileName)
64
+ await fsp.copyFile(filePath, destPath)
65
+ return destPath
66
+ } catch (error) {
67
+ log.E(`Error copying file to temp directory: ${error.message}`)
68
+ throw error
69
+ }
70
+ }
71
+
72
+ // Dynamically imports the javascript file at filePath, which can be relative or absolute
73
+ export async function importJsFile(filePath) {
74
+ if (!path.isAbsolute(filePath)) {
75
+ filePath = path.resolve(CWD, filePath)
76
+ }
77
+ // Convert the file path to a proper URL
78
+ const fileUrl = pathToFileURL(filePath)
79
+ return await import(fileUrl)
80
+ }
81
+
82
+ export function normalizeOutputPath({ dir, filename, normalize }) {
83
+ return path.join(dir, normalize ? filename.toLowerCase() : filename)
84
+ }
85
+
86
+ export function writeJsonFile(filePath, data, log) {
87
+ assertValidPath(filePath)
88
+ assertIsObj(data)
89
+ log.V(`Preparing to write ${filePath}...`)
90
+
91
+ // Create normalized version of data with consistent key encoding
92
+ log.D(`Normalizing data...`)
93
+ const normalizedData = {}
94
+ for (const [ key, value ] of Object.entries(data)) {
95
+ normalizedData[normalizeKey(key)] = value
96
+ }
97
+ log.D(`Done.`)
98
+
99
+ try {
100
+ const dir = path.dirname(filePath)
101
+ log.D(`Ensuring directory ${dir} exists...`)
102
+ if (!dirExists(dir, log)) {
103
+ log.D(`Directory ${dir} did not exist; creating...`)
104
+ fs.mkdirSync(dir, { recursive: true })
105
+ }
106
+ log.D(`Done.`)
107
+ } catch (err) {
108
+ log.E(err)
109
+ }
110
+
111
+ log.V(`Writing ${filePath}...`)
112
+ try {
113
+ fs.writeFileSync(filePath, JSON.stringify(normalizedData, null, 2), 'utf8')
114
+ } catch (err) {
115
+ log.E(err)
116
+ }
117
+ log.D(`Done.`)
118
+ }
119
+
120
+ export function dirExists(dir, log) {
121
+ try {
122
+ log.D(`fetching stats for ${dir}...`)
123
+ return fs.statSync(dir).isDirectory()
124
+ } catch (error) {
125
+ log.E(error)
126
+ return false
127
+ }
128
+ }
129
+
@@ -0,0 +1,135 @@
1
+ import * as fsp from 'fs/promises'
2
+ import * as locale from 'locale-codes'
3
+ import * as path from 'path'
4
+ import { obj2Str, replaceStringVarsWithObjectValues } from '../utils.js'
5
+ import { fileExists, readJsonFile } from '../io.js'
6
+ import { LANGTAG_DEFAULT } from '../consts.js'
7
+
8
+ const LocalizationMap = {}
9
+
10
+ export async function initLocalizer({ defaultAppLanguage, appLanguage, srcDir, log }) {
11
+ let lang
12
+
13
+ // Always load the default language, as a fallback
14
+ await addLocalizationDataForLanguage({ lang: defaultAppLanguage, srcPath: path.resolve(srcDir, `${defaultAppLanguage}.json`), log })
15
+
16
+ if (appLanguage?.length && appLanguage !== defaultAppLanguage) {
17
+ try {
18
+ if (!isBcp47LanguageTagValid(appLanguage)) {
19
+ log.W(`"${appLanguage}" is not a valid BCP47 language`)
20
+ throw new Error() // Print another warning and use default language
21
+ }
22
+ await addLocalizationDataForLanguage({ lang: appLanguage, srcPath: path.resolve(srcDir, `${appLanguage}.json`), log })
23
+ lang = appLanguage
24
+ } catch (err) {
25
+ log.W(`No localization data found for language "${appLanguage}"; falling back to "${defaultAppLanguage}"...`)
26
+ lang = defaultAppLanguage
27
+ }
28
+ } else {
29
+ lang = defaultAppLanguage
30
+ }
31
+
32
+ return lang
33
+ }
34
+
35
+ export function isBcp47LanguageTagValid(tag) {
36
+ return locale.getByTag(tag)
37
+ }
38
+
39
+ export function isDefaultLanguage(tag) {
40
+ return tag === LANGTAG_DEFAULT
41
+ }
42
+
43
+ export function localizeFormatted({ token, data, lang, fallbackToken, log }) {
44
+ //assertIsObj(data)
45
+ //assertIsNonEmptyString(lang)
46
+ //assertValidBcp47LanguageTag(lang)
47
+
48
+ // We don't use fallbackToken directly in our call to localize(), because we don't want to inadvertently format in odd ways
49
+ // But let's see how we end up using this function; for now we just directly localize the fallback below, if localize() fails here
50
+ const str = localize({ token, lang, log })
51
+
52
+ // Directly attempt to localize the fallback (or returns '')
53
+ if (!str?.length) {
54
+ const fallbackResult = localize({ token: fallbackToken, lang, log })
55
+ if (fallbackResult.indexOf('%%') >= 0) {
56
+ log.W(`'fallbackToken' should not include formatting variables; not currently supported; string="${fallbackResult}"`)
57
+ }
58
+ return fallbackResult
59
+ }
60
+
61
+ const warnings = []
62
+ const result = replaceStringVarsWithObjectValues({
63
+ format: str, data, outWarningsArray: warnings
64
+ })
65
+
66
+ if (warnings.length) warnings.forEach(w => log.W(`[localizeFormatted] warning: ${w.message} (code=${w.code})`))
67
+
68
+ return result
69
+ }
70
+
71
+ export function localize({ token, lang, fallbackToken, log }) {
72
+ log.D(`[localize] ${obj2Str({ token, lang, fallbackToken })}`)
73
+
74
+ if (!isBcp47LanguageTagValid(lang)) {
75
+ log.D(`[localize] Invalid language "${lang}" passed; falling back to default language...`)
76
+ lang = LANGTAG_DEFAULT
77
+ }
78
+
79
+ if (!token) token = ''
80
+
81
+ if (token.startsWith('#')) {
82
+ log.D(`[localize] removed leading '#' character`)
83
+ token = token.substring(1)
84
+ }
85
+
86
+ if (!token.length && fallbackToken) {
87
+ log.D(`[localize] token was empty; trying fallback...`)
88
+ return localize({ token: fallbackToken, lang, log })
89
+ }
90
+
91
+ if (!(lang in LocalizationMap)) {
92
+ log.W(`[localize] lang "${lang}" was not in LocalizationMap`)
93
+
94
+ if (!isDefaultLanguage(lang)) {
95
+ return localize({ token, lang: LANGTAG_DEFAULT, log })
96
+ }
97
+ return ''
98
+ } else if (!(token in LocalizationMap[lang])) {
99
+ log.D(`[localize] token "${token}" was not in LocalizationMap[${lang}]`)
100
+
101
+ // If it didn't exist, check the fallback in the same language
102
+ if (fallbackToken) {
103
+ log.D(`[localize] falling back to token ${fallbackToken}`)
104
+ return localize({ token: fallbackToken, lang, log })
105
+ }
106
+
107
+ // If no fallback in the same language, attempt to find an english version of 'token'
108
+ if (!isDefaultLanguage(lang)) {
109
+ log.D(`[localize] attempting to fall back to english`)
110
+ // This will get the fallback in english if not found in the requested language
111
+ return localize({ token, lang: LANGTAG_DEFAULT, fallbackToken, log })
112
+ }
113
+
114
+ log.W(`Failed to find localization string for language="${lang}", token="${token}"`)
115
+
116
+ return ''
117
+ }
118
+
119
+ const result = LocalizationMap[lang][token]
120
+ log.D(`[localize] success; found localization string; ${obj2Str({ token, lang, result })}`)
121
+
122
+ return result
123
+ }
124
+
125
+ async function addLocalizationDataForLanguage({ lang, srcPath, log }) {
126
+ //assertValidBcp47LanguageTag(lang)
127
+ //assertIsObj(data)
128
+ //assertIsObj(LocalizationMap)
129
+
130
+ const data = await readJsonFile(srcPath)
131
+ if (!data) throw new Error(`Failed to load localization file "${srcPath}"`)
132
+
133
+ LocalizationMap[lang] = LocalizationMap.lang ?? {}
134
+ LocalizationMap[lang] = { ...LocalizationMap.lang, ...data }
135
+ }
package/src/logging.js ADDED
@@ -0,0 +1,35 @@
1
+ export function createLog() {
2
+ return {
3
+ // T/D/V blackholed until program options are parsed
4
+ T: () => { },
5
+ D: () => { },
6
+ V: () => { },
7
+
8
+ E: function(...args) {
9
+ console.error(...args)
10
+ },
11
+ W: function(...args) {
12
+ console.warn(...args)
13
+ },
14
+ I: function(...args) {
15
+ console.log(...args)
16
+ },
17
+ }
18
+ }
19
+
20
+ export function initLogFromOptions({ options, log }) {
21
+ // Init optional logging functions
22
+ log.V = (options.trace || options.debug || options.verbose) ? function(...args) {
23
+ console.log(...args)
24
+ } : () => {
25
+ }
26
+ log.D = (options.trace || options.debug) ? function(...args) {
27
+ console.debug(...args)
28
+ } : () => {
29
+ }
30
+ log.T = options.trace ? function(...args) {
31
+ console.debug(...args)
32
+ } : () => {
33
+ }
34
+ }
35
+
package/src/logo.js ADDED
@@ -0,0 +1,22 @@
1
+ import figlet from 'figlet'
2
+ import gradient from 'gradient-string'
3
+ import * as path from 'path'
4
+ import * as fsp from 'fs/promises'
5
+
6
+ export async function printLogo({ fontsSrcDir, tagline, log }) {
7
+ const fontName = 'THIS.flf'
8
+ const fontPath = path.resolve(fontsSrcDir, fontName)
9
+ const fontData = await fsp.readFile(fontPath, 'utf8')
10
+ figlet.parseFont(fontName, fontData)
11
+ const asciiTitle = figlet.textSync('ALT', {
12
+ font: fontName,
13
+ horizontalLayout: 'full',
14
+ verticalLayout: 'default',
15
+ })
16
+
17
+ log.I(`\n${gradient([
18
+ '#000FFF',
19
+ '#ed00b1'
20
+ ])(asciiTitle)}\n`)
21
+ }
22
+
package/src/main.mjs ADDED
@@ -0,0 +1,103 @@
1
+ import * as path from 'path'
2
+ import { program } from 'commander'
3
+ import { fileURLToPath } from 'url'
4
+ import { initLocalizer } from './localizer/localize.js'
5
+ import {
6
+ DEFAULT_CONFIG_FILENAME,
7
+ ENV_VARS,
8
+ LANGTAG_DEFAULT,
9
+ LOCALIZATION_SRC_DIR,
10
+ } from './consts.js'
11
+ import { readJsonFile } from './io.js'
12
+ import { printLogo } from './logo.js'
13
+ import { createLog, initLogFromOptions } from './logging.js'
14
+ import { keyList, languageList } from './options.js'
15
+ import { runTranslation } from './translate.js'
16
+ import { registerSignalHandlers } from './shutdown.js'
17
+
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
19
+
20
+ // Main function
21
+ export async function run() {
22
+ const log = createLog()
23
+
24
+ const appState = {
25
+ lang: null, // The app language, for output display (unrelated to translator)
26
+ tmpDir: null,
27
+ filesToWrite: {}, // Map of file path => JSON data to write
28
+ errors: [],
29
+ log
30
+ }
31
+
32
+ try {
33
+ registerSignalHandlers(appState)
34
+
35
+ appState.lang = await initLocalizer({
36
+ defaultAppLanguage: LANGTAG_DEFAULT,
37
+ appLanguage: process.env?.ALT_LANGUAGE,
38
+ srcDir: path.resolve(__dirname, LOCALIZATION_SRC_DIR),
39
+ log
40
+ })
41
+
42
+ const p = await readJsonFile(path.resolve(__dirname, '../package.json'))
43
+ if (!p) throw new Error(`Couldn't read 'package.json'`)
44
+
45
+ // Define CLI options
46
+ program
47
+ .version(p.version)
48
+ .description(p.description)
49
+ .on('--help', () => {
50
+ log.I()
51
+ log.I('Environment variables:')
52
+ ENV_VARS.forEach(v => {
53
+ log.I(` ${v.name.padEnd(37)} ${v.description}`)
54
+ })
55
+ })
56
+ .requiredOption('-r, --reference-file <path>', 'Path to reference JSONC file (default language)')
57
+ .option('-rl, --reference-language <language>', `The reference file's language; overrides any 'referenceLanguage' config setting`)
58
+ .option('-p, --provider <name>', `AI provider to use for translations (anthropic, openai); overrides any 'provider' config setting`)
59
+ .option('-o, --output-dir <path>', 'Output directory for localized files')
60
+ .option('-l, --target-languages <list>', `Comma-separated list of language codes; overrides any 'targetLanguages' config setting`, value => languageList(value, log))
61
+ .option('-k, --keys <list>', 'Comma-separated list of keys to process', keyList)
62
+ .option('-j, --reference-var-name <var name>', `The exported variable in the reference file, e.g. export default = {...} you'd use 'default'`, 'default')
63
+ .option('-f, --force', 'Force regeneration of all translations', false)
64
+ .option('-rtw, --realtime-writes', 'Write updates to disk immediately, rather than on shutdown', false)
65
+ .option('-m, --app-context-message <message>', `Description of your app to give context. Passed with each translation request; overrides any 'appContextMessage' config setting`)
66
+ .option('-y, --tty', 'Use tty/simple renderer; useful for CI', false)
67
+ .option('-c, --config-file <path>', `Path to config file; defaults to <output dir>/${DEFAULT_CONFIG_FILENAME}`)
68
+ .option('-x, --max-retries <integer>', 'Maximum retries on failure', 3)
69
+ .option('-n, --normalize-output-filenames', `Normalizes output filenames (to all lower-case); overrides any 'normalizeOutputFilenames' in config setting`, false)
70
+ .option('-v, --verbose', `Enables verbose spew`, false)
71
+ .option('-d, --debug', `Enables debug spew`, false)
72
+ .option('-t, --trace', `Enables trace spew`, false)
73
+ .option('--context-prefix <value>', `String to be prefixed to all keys to search for additional context, which are passed along to the AI for context`)
74
+ .option('--context-suffix <value>', `String to be suffixed to all keys to search for additional context, which are passed along to the AI for context`)
75
+ .option('--look-for-context-data', `If specified, ALT will pass any context data specified in the reference file to the AI provider for translation. At least one of --contextPrefix or --contextSuffix must be specified`, false)
76
+ .hook('preAction', (thisCommand) => {
77
+ const opts = thisCommand.opts()
78
+ if (opts.lookForContextData && !(opts.contextPrefix?.length || opts.contextSuffix?.length)) {
79
+ thisCommand.error('--lookForContextData requires at least 1 of --contextPrefix or --contextSuffix be defined and non-empty')
80
+ }
81
+ })
82
+ .action(async (thisCommand) => {
83
+ }) // Dummy action() for preAction
84
+
85
+ program
86
+ .command('translate', { isDefault: true }) // This makes it the default command
87
+ .action(async () => {
88
+ const options = program.opts()
89
+ initLogFromOptions({ options, log })
90
+ await runTranslation({ appState, options, log })
91
+ })
92
+
93
+ program.parse(process.argv)
94
+
95
+ await printLogo({
96
+ fontsSrcDir: path.resolve(__dirname, '../assets/figlet-fonts/'),
97
+ tagline: p.description,
98
+ log
99
+ })
100
+ } catch (error) {
101
+ log.E(error)
102
+ }
103
+ }
package/src/options.js ADDED
@@ -0,0 +1,18 @@
1
+ import { isBcp47LanguageTagValid } from './localizer/localize.js'
2
+ import { unique } from './utils.js'
3
+
4
+ // Helper function to parse comma-separated list
5
+ export function languageList(value, log) {
6
+ const languages = unique(value.split(',').map(item => item.trim()))
7
+ const invalid = languages.filter(tag => !isBcp47LanguageTagValid(tag))
8
+ if (invalid.length) {
9
+ log.E(`Found invalid language(s): ${invalid.join(', ')}`)
10
+ process.exit(1)
11
+ }
12
+ return languages
13
+ }
14
+
15
+ export function keyList(value) {
16
+ return value.split(',').map(item => item.trim())
17
+ }
18
+
@@ -0,0 +1,17 @@
1
+ import * as path from 'path'
2
+ import { importJsFile } from './io.js'
3
+
4
+ export async function loadTranslationProvider({__dirname, providerName, log}) {
5
+ const apiKeyName = `${providerName.toUpperCase()}_API_KEY`
6
+ const apiKey = process.env[apiKeyName]
7
+ if (!apiKey?.length) {
8
+ log.E(`${apiKeyName} environment variable is not set`)
9
+ process.exit(1)
10
+ }
11
+
12
+ return {
13
+ apiKey,
14
+ api: await importJsFile(path.resolve(__dirname, `providers/${providerName}.mjs`)),
15
+ }
16
+ }
17
+
@@ -0,0 +1,39 @@
1
+ export function name() {
2
+ return 'Anthropic'
3
+ }
4
+
5
+ export function getTranslationRequestDetails({ messages, apiKey, log }) {
6
+ return {
7
+ url: 'https://api.anthropic.com/v1/messages',
8
+ params: {
9
+ model: 'claude-3-7-sonnet-20250219',
10
+ max_tokens: 1024,
11
+ messages: messages.map(m => ({ role: 'user', content: m })),
12
+ },
13
+ config: {
14
+ headers: {
15
+ 'Content-Type': 'application/json',
16
+ 'x-api-key': apiKey,
17
+ 'anthropic-version': '2023-06-01',
18
+ },
19
+ },
20
+ }
21
+ }
22
+
23
+ export function getResult(response, log) {
24
+ return response.data.content[0].text.trim()
25
+ }
26
+
27
+ function getHeader(headers, name) {
28
+ return headers[name] || headers.get?.(name)
29
+ }
30
+
31
+ export function getSleepInterval(headers, log) {
32
+ log.T(headers)
33
+ if (getHeader(headers, 'x-should-retry') !== 'true')
34
+ return 0
35
+
36
+ const retryAfter = parseInt(getHeader(headers, 'retry-after'))
37
+ log.D('retryAfter', retryAfter)
38
+ return 1000 * retryAfter + 200
39
+ }
@@ -0,0 +1,30 @@
1
+ export function name() {
2
+ return 'OpenAI'
3
+ }
4
+
5
+ export function getTranslationRequestDetails({ messages, apiKey, log }) {
6
+ return {
7
+ url: 'https://api.openai.com/v1/chat/completions',
8
+ params: {
9
+ model: 'gpt-4-turbo',
10
+ messages: messages.map((m, idx) => {
11
+ return {
12
+ role: idx === messages.length - 1 ? 'user' : 'system',
13
+ content: m,
14
+ }
15
+ }),
16
+ temperature: 0.3,
17
+ max_tokens: 1024,
18
+ },
19
+ config: {
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ 'Authorization': `Bearer ${apiKey}`,
23
+ },
24
+ },
25
+ }
26
+ }
27
+
28
+ export function getResult(response) {
29
+ return response.data.choices[0].message.content.trim()
30
+ }