glotstack 0.0.4 → 0.0.6
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.
- package/dist/cli.js +94 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.js +12 -7
- package/dist/index.js.map +1 -1
- package/eslint-raw-string.mjs +11 -0
- package/package-lock.json +4356 -0
- package/package.json +8 -6
- package/src/cli.tsx +82 -34
- package/src/index.tsx +10 -8
package/package.json
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glotstack",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "JD Cumpson",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"private": false,
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"commander": "^13.1.0"
|
|
10
|
+
"commander": "^13.1.0",
|
|
11
|
+
"eslint": "^9.26.0"
|
|
11
12
|
},
|
|
12
13
|
"devDependencies": {
|
|
14
|
+
"@eslint/js": "^9.26.0",
|
|
13
15
|
"@types/js-yaml": "^4.0.9",
|
|
14
16
|
"@types/node": "^22.15.17",
|
|
15
|
-
"@types/react": "^19.1.
|
|
17
|
+
"@types/react": "^19.1.4",
|
|
18
|
+
"globals": "^16.1.0",
|
|
16
19
|
"js-yaml": "^4.1.0",
|
|
17
20
|
"typescript": "5.4.4",
|
|
18
|
-
"typescript-eslint": "^8.
|
|
21
|
+
"typescript-eslint": "^8.32.1"
|
|
19
22
|
},
|
|
20
23
|
"peerDependencies": {
|
|
21
|
-
"
|
|
22
|
-
"react": "^18.3.1"
|
|
24
|
+
"react": "^19.1.0"
|
|
23
25
|
},
|
|
24
26
|
"resolutions": {
|
|
25
27
|
"react": "18.3.1"
|
package/src/cli.tsx
CHANGED
|
@@ -1,17 +1,83 @@
|
|
|
1
1
|
import { Command, program } from 'commander'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import { promises as fs } from 'fs'
|
|
3
|
+
import { promises as fs, read } from 'fs'
|
|
4
4
|
import { findGlotstackConfig } from './util/findConfig'
|
|
5
5
|
import { cwd } from 'process'
|
|
6
6
|
import { merge } from './util/object'
|
|
7
7
|
import { loadYaml } from './util/yaml'
|
|
8
|
+
import eslint from 'eslint'
|
|
9
|
+
import * as readline from "node:readline/promises"
|
|
10
|
+
import { stdin as input, stdout as output } from "node:process"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
const fetchGlotstack = async (apiOrigin: string, apiKey: string, body: object) => {
|
|
14
|
+
const url = `${apiOrigin}/api/translations`
|
|
15
|
+
console.info(`Fetching translations from: ${url}`)
|
|
16
|
+
|
|
17
|
+
const headers = {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(url, { method: 'POST', body: JSON.stringify(body), headers })
|
|
24
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
|
25
|
+
return res.json()
|
|
26
|
+
// fs.writeFile(`${outputDir}/source.json`, JSON.stringify(json, null, 2))
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error('Fetch failed:', err)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
8
32
|
|
|
9
33
|
|
|
10
34
|
async function run(args: string[]) {
|
|
35
|
+
program
|
|
36
|
+
.command('extract-translations')
|
|
37
|
+
.option('--source-path [path]', 'to source files root directory', '.')
|
|
38
|
+
.option('--api-origin [url]', 'glotstack api origin', process.env.GLOTSTACK_HOST ?? 'https://glotstack.ai')
|
|
39
|
+
.option('--yes', 'skip confirm checks', false)
|
|
40
|
+
.action(async (options: Record<string, any>) => {
|
|
41
|
+
if (!options.apiOrigin) {
|
|
42
|
+
throw new Error('apiOrigin must be specified')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const linter = new eslint.ESLint({ overrideConfigFile: path.join(__dirname, '..', 'eslint-raw-string.mjs') })
|
|
46
|
+
const results = await linter.lintFiles(["./**/*"])
|
|
47
|
+
const filesWithIssues = results
|
|
48
|
+
.filter((r) => r.errorCount + r.warningCount > 0)
|
|
49
|
+
.map((r) => r.filePath)
|
|
50
|
+
|
|
51
|
+
const rl = readline.createInterface({ input, output })
|
|
52
|
+
|
|
53
|
+
const askToSend = async (): Promise<boolean> => {
|
|
54
|
+
if (options.yes) {
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
const response = await rl.question(`Your source are going to be sent to our LLM -- they should not contain any secrets. Proceed? (yes/no):`)
|
|
58
|
+
if (response === 'yes') {
|
|
59
|
+
return true
|
|
60
|
+
} else if (response !== 'no') {
|
|
61
|
+
console.error('Please respond with yes or no.')
|
|
62
|
+
return askToSend()
|
|
63
|
+
} else {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const send = await askToSend()
|
|
69
|
+
if (send) {
|
|
70
|
+
console.info('Sending files to LLM')
|
|
71
|
+
rl.close()
|
|
72
|
+
} else {
|
|
73
|
+
rl.close()
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
11
77
|
program
|
|
12
78
|
.command('get-translations')
|
|
13
79
|
.option('--source-path [path]', 'path to en-US.json (or your canonical source json)')
|
|
14
|
-
.option('--api-origin [url]', 'glotstack api origin', '
|
|
80
|
+
.option('--api-origin [url]', 'glotstack api origin', process.env.GLOTSTACK_HOST ?? 'https://glotstack.ai')
|
|
15
81
|
.option('--output-dir [path]', 'path to output directory')
|
|
16
82
|
.option('--api-key [key]', 'api key for glotstack.ai')
|
|
17
83
|
.option('--project-id [id]', '(optional) specific project to use')
|
|
@@ -26,16 +92,16 @@ async function run(args: string[]) {
|
|
|
26
92
|
const text = await fs.readFile(configPath, 'utf-8')
|
|
27
93
|
config = JSON.parse(text)
|
|
28
94
|
console.info('Loaded config file', config)
|
|
29
|
-
} catch(err) {
|
|
95
|
+
} catch (err) {
|
|
30
96
|
//pass
|
|
31
97
|
}
|
|
32
98
|
}
|
|
33
99
|
|
|
34
100
|
const resolved = merge(config, options, { outputLocales })
|
|
35
|
-
const { apiOrigin, sourcePath, outputDir, projectId} = resolved
|
|
101
|
+
const { apiOrigin, sourcePath, outputDir, projectId } = resolved
|
|
36
102
|
|
|
37
103
|
if ((resolved.outputLocales as string[]).includes('en-US')) {
|
|
38
|
-
console.warn('en-US detected in outputLocales, removing')
|
|
104
|
+
console.warn('en-US detected in outputLocales, removing')
|
|
39
105
|
resolved.outputLocales = resolved.outputLocales.filter((x: string) => x !== 'en-US')
|
|
40
106
|
}
|
|
41
107
|
|
|
@@ -49,52 +115,34 @@ async function run(args: string[]) {
|
|
|
49
115
|
throw new Error('outputDir must be specified')
|
|
50
116
|
}
|
|
51
117
|
|
|
52
|
-
const url = `${apiOrigin}/api/translations`
|
|
53
|
-
console.info(`Fetching translations from: ${url}`)
|
|
54
|
-
|
|
55
118
|
const absPath = path.resolve(sourcePath)
|
|
56
119
|
const fileContent = await fs.readFile(absPath, 'utf-8')
|
|
57
120
|
|
|
58
121
|
let json = null
|
|
59
122
|
try {
|
|
60
123
|
json = loadYaml(fileContent)
|
|
61
|
-
} catch(err) {
|
|
124
|
+
} catch (err) {
|
|
62
125
|
try {
|
|
63
126
|
json = JSON.parse(fileContent)
|
|
64
|
-
} catch(err) {
|
|
127
|
+
} catch (err) {
|
|
65
128
|
console.error('Unable to parse source file ', absPath, err)
|
|
66
|
-
throw err
|
|
129
|
+
throw err
|
|
67
130
|
}
|
|
68
131
|
}
|
|
69
132
|
|
|
70
133
|
const body = {
|
|
71
134
|
locales: resolved.outputLocales,
|
|
72
135
|
translations: json,
|
|
73
|
-
...{ ... (projectId != null ? {projectId} : {})},
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const headers ={
|
|
77
|
-
'Content-Type': 'application/json',
|
|
78
|
-
'Authorization': `Bearer ${resolved.apiKey}` ,
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
const res = await fetch(url, { method: 'POST', body: JSON.stringify(body), headers })
|
|
83
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
|
84
|
-
|
|
85
|
-
const data = await res.json()
|
|
86
|
-
console.info('Received translations:', data)
|
|
87
|
-
Object.entries(data.data).map(([key, val]) => {
|
|
88
|
-
const p = `${outputDir}/${key}.json`
|
|
89
|
-
console.info(`Writing file ${p}`)
|
|
90
|
-
fs.writeFile(`${outputDir}/${key}.json`, JSON.stringify(val, null, 2))
|
|
91
|
-
})
|
|
92
|
-
// fs.writeFile(`${outputDir}/source.json`, JSON.stringify(json, null, 2))
|
|
93
|
-
} catch (err) {
|
|
94
|
-
console.error('Fetch failed:', err)
|
|
95
|
-
process.exit(1)
|
|
136
|
+
...{ ... (projectId != null ? { projectId } : {}) },
|
|
96
137
|
}
|
|
97
138
|
|
|
139
|
+
const data = await fetchGlotstack(apiOrigin, resolved.apiKey, body)
|
|
140
|
+
console.info('Received translations:', data)
|
|
141
|
+
Object.entries(data.data).map(([key, val]) => {
|
|
142
|
+
const p = `${outputDir}/${key}.json`
|
|
143
|
+
console.info(`Writing file ${p}`)
|
|
144
|
+
fs.writeFile(`${outputDir}/${key}.json`, JSON.stringify(val, null, 2))
|
|
145
|
+
})
|
|
98
146
|
})
|
|
99
147
|
|
|
100
148
|
await program.parseAsync(args)
|
package/src/index.tsx
CHANGED
|
@@ -68,12 +68,12 @@ export const GlotstackProvider = ({ children, initialLocale, initialTranslations
|
|
|
68
68
|
if (initialLocale == null) {
|
|
69
69
|
throw new Error('initialLocale must be set')
|
|
70
70
|
}
|
|
71
|
-
|
|
72
71
|
const [locale, setLocale] = React.useState<LocaleRegion>(initialLocale)
|
|
73
|
-
const
|
|
72
|
+
const translationsRef = React.useRef(initialTranslations)
|
|
74
73
|
const loadingRef = React.useRef<Record<string, Promise<Translations>>>({})
|
|
75
74
|
|
|
76
75
|
const loadTranslations = React.useCallback(async (locale: string) => {
|
|
76
|
+
// TODO: if translations are loaded only reload if some condition is
|
|
77
77
|
try {
|
|
78
78
|
if (loadingRef.current?.[locale] != null) {
|
|
79
79
|
return (await loadingRef.current?.[locale])
|
|
@@ -84,14 +84,16 @@ export const GlotstackProvider = ({ children, initialLocale, initialTranslations
|
|
|
84
84
|
if (result == null) {
|
|
85
85
|
throw new Error(`Failed to load translation ${locale} ${JSON.stringify(result)}`)
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
if (translationsRef.current) {
|
|
88
|
+
translationsRef.current[locale] = result
|
|
89
|
+
}
|
|
88
90
|
onTranslationLoaded?.(locale, result)
|
|
89
91
|
return result
|
|
90
92
|
} catch (err) {
|
|
91
93
|
console.error('Unable to import translations', err)
|
|
92
94
|
throw err
|
|
93
95
|
}
|
|
94
|
-
}, [importMethod,
|
|
96
|
+
}, [importMethod, onTranslationLoaded])
|
|
95
97
|
|
|
96
98
|
React.useEffect(() => {
|
|
97
99
|
const run = async () => {
|
|
@@ -106,7 +108,7 @@ export const GlotstackProvider = ({ children, initialLocale, initialTranslations
|
|
|
106
108
|
const context = React.useMemo(() => {
|
|
107
109
|
return {
|
|
108
110
|
setLocale,
|
|
109
|
-
translations:
|
|
111
|
+
translations: translationsRef.current ?? {},
|
|
110
112
|
locale,
|
|
111
113
|
importMethod,
|
|
112
114
|
loadTranslations,
|
|
@@ -116,12 +118,12 @@ export const GlotstackProvider = ({ children, initialLocale, initialTranslations
|
|
|
116
118
|
return
|
|
117
119
|
}
|
|
118
120
|
loadTranslations(opts?.locale)
|
|
119
|
-
}, [locale])
|
|
120
|
-
return access(key, locale,
|
|
121
|
+
}, [locale, opts?.locale])
|
|
122
|
+
return access(key, opts?.locale ?? locale, translationsRef.current ?? {})
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
}
|
|
124
|
-
}, [locale, importMethod
|
|
126
|
+
}, [locale, importMethod])
|
|
125
127
|
|
|
126
128
|
return <GlotstackContext.Provider value={context}>
|
|
127
129
|
{children}
|