glotstack 0.0.11 → 0.0.14
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/README.md +207 -0
- package/dist/cli.js +34 -64
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -30
- package/dist/index.js +74 -57
- package/dist/index.js.map +1 -1
- package/dist/logging.d.ts +15 -0
- package/dist/logging.js +44 -0
- package/dist/logging.js.map +1 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/util/fetchGlotstack.d.ts +2 -0
- package/dist/util/fetchGlotstack.js +36 -0
- package/dist/util/fetchGlotstack.js.map +1 -0
- package/dist/util/findConfig.js +8 -4
- package/dist/util/findConfig.js.map +1 -1
- package/dist/util/object.d.ts +1 -0
- package/dist/util/object.js +21 -1
- package/dist/util/object.js.map +1 -1
- package/dist/util/waitForFile.d.ts +2 -0
- package/dist/util/waitForFile.js +58 -0
- package/dist/util/waitForFile.js.map +1 -0
- package/package.json +21 -12
- package/.prettierrc +0 -10
- package/eslint-raw-string.mjs +0 -15
- package/scripts/fix-shebang.sh +0 -28
- package/src/cli.tsx +0 -295
- package/src/index.tsx +0 -518
- package/src/util/findConfig.ts +0 -97
- package/src/util/object.ts +0 -47
- package/src/util/yaml.ts +0 -11
- package/tsconfig.json +0 -23
- package/types/global.d.ts +0 -7
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glotstack",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"main": "dist/index.js",
|
|
5
|
-
"types": "dist/index.d.ts",
|
|
3
|
+
"version": "0.0.14",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
6
|
+
"react-native": "./dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
6
10
|
"author": "JD Cumpson",
|
|
7
11
|
"license": "MIT",
|
|
8
|
-
"private": false,
|
|
9
12
|
"dependencies": {
|
|
10
13
|
"@eslint/js": "^9.26.0",
|
|
11
14
|
"commander": "^13.1.0",
|
|
@@ -18,25 +21,31 @@
|
|
|
18
21
|
"devDependencies": {
|
|
19
22
|
"@types/js-yaml": "^4.0.9",
|
|
20
23
|
"@types/node": "^22.15.17",
|
|
21
|
-
"@types/react": "^
|
|
22
|
-
"@types/react-dom": "^
|
|
24
|
+
"@types/react": "^19.1.0",
|
|
25
|
+
"@types/react-dom": "^19.1.0",
|
|
23
26
|
"globals": "^16.1.0",
|
|
24
27
|
"js-yaml": "^4.1.0",
|
|
25
28
|
"nodemon": "^3.1.10",
|
|
29
|
+
"react": "19.1.0",
|
|
26
30
|
"typescript": "5.4.4",
|
|
27
31
|
"typescript-eslint": "^8.32.1"
|
|
28
32
|
},
|
|
29
33
|
"peerDependencies": {
|
|
30
|
-
"react": "
|
|
31
|
-
"react-dom": "
|
|
34
|
+
"react": ">=19",
|
|
35
|
+
"react-dom": ">=19",
|
|
36
|
+
"react-native": ">=0.81"
|
|
32
37
|
},
|
|
33
|
-
"
|
|
34
|
-
"
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"react-native": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
35
42
|
},
|
|
43
|
+
"bin": "dist/cli.js",
|
|
36
44
|
"scripts": {
|
|
37
45
|
"build": "tsc && scripts/fix-shebang.sh dist/cli.js",
|
|
38
46
|
"watch": "nodemon --watch src --ext ts,tsx,mjs,json --exec \"bash -c 'yarn build'\"",
|
|
39
47
|
"prepublishOnly": "yarn build",
|
|
40
48
|
"glotstack": "node dist/cli.js"
|
|
41
|
-
}
|
|
42
|
-
|
|
49
|
+
},
|
|
50
|
+
"packageManager": "yarn@4.12.0"
|
|
51
|
+
}
|
package/.prettierrc
DELETED
package/eslint-raw-string.mjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import globals from "globals";
|
|
2
|
-
import tseslint from "typescript-eslint";
|
|
3
|
-
import { defineConfig, globalIgnores } from "eslint/config";
|
|
4
|
-
import i18next from 'eslint-plugin-i18next';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export default defineConfig([
|
|
8
|
-
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], languageOptions: { globals: globals.browser } },
|
|
9
|
-
globalIgnores([
|
|
10
|
-
'node_modules/*', // ignore node modules
|
|
11
|
-
'**/*.d.ts', // ignore type definitions
|
|
12
|
-
]),
|
|
13
|
-
tseslint.configs.base,
|
|
14
|
-
i18next.configs['flat/recommended'],
|
|
15
|
-
]);
|
package/scripts/fix-shebang.sh
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
|
|
3
|
-
FILE="$1"
|
|
4
|
-
SHEBANG='#!/usr/bin/env node'
|
|
5
|
-
|
|
6
|
-
if [ -z "$FILE" ]; then
|
|
7
|
-
echo "Usage: $0 <file>"
|
|
8
|
-
exit 1
|
|
9
|
-
fi
|
|
10
|
-
|
|
11
|
-
if [ ! -f "$FILE" ]; then
|
|
12
|
-
echo "File not found: $FILE"
|
|
13
|
-
exit 1
|
|
14
|
-
fi
|
|
15
|
-
|
|
16
|
-
FIRST_LINE=$(head -n 1 "$FILE")
|
|
17
|
-
|
|
18
|
-
if [ "$FIRST_LINE" != "$SHEBANG" ]; then
|
|
19
|
-
echo "Injecting shebang into $FILE"
|
|
20
|
-
TMP_FILE=$(mktemp)
|
|
21
|
-
echo "$SHEBANG" > "$TMP_FILE"
|
|
22
|
-
cat "$FILE" >> "$TMP_FILE"
|
|
23
|
-
mv "$TMP_FILE" "$FILE"
|
|
24
|
-
else
|
|
25
|
-
echo "Shebang already present in $FILE"
|
|
26
|
-
fi
|
|
27
|
-
|
|
28
|
-
chmod +x "$FILE"
|
package/src/cli.tsx
DELETED
|
@@ -1,295 +0,0 @@
|
|
|
1
|
-
import { Command, program } from 'commander'
|
|
2
|
-
import path from 'path'
|
|
3
|
-
import { promises as fs, createReadStream } from 'fs'
|
|
4
|
-
import { findGlotstackConfig, GlotstackConfig } from './util/findConfig'
|
|
5
|
-
import { cwd } from 'process'
|
|
6
|
-
import { merge } from './util/object'
|
|
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
|
-
import { Translations } from 'src'
|
|
12
|
-
import { fetch } from 'undici'
|
|
13
|
-
import { FormData } from 'undici'
|
|
14
|
-
import { openAsBlob } from 'node:fs'
|
|
15
|
-
import { readdir, readFile, writeFile } from 'node:fs/promises'
|
|
16
|
-
import { resolve } from 'node:path'
|
|
17
|
-
|
|
18
|
-
const fetchGlotstack = async function <T>(url: string, apiKey: string, body: Record<string, any> | FormData, overrideHeaders?: Record<string, any>): Promise<T> {
|
|
19
|
-
console.info(`Fetching glotstack.ai: ${url}`)
|
|
20
|
-
|
|
21
|
-
const headers: Record<string, any> = {
|
|
22
|
-
'authorization': `Bearer ${apiKey}`,
|
|
23
|
-
...(overrideHeaders == null ? {} : overrideHeaders),
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let payloadBody: FormData | string
|
|
27
|
-
|
|
28
|
-
if (!(body instanceof FormData)) {
|
|
29
|
-
headers['content-type'] = 'application/json'
|
|
30
|
-
payloadBody = JSON.stringify(body)
|
|
31
|
-
} else {
|
|
32
|
-
payloadBody = body
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const res = await fetch(url, { method: 'POST', body: payloadBody, headers })
|
|
37
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`)
|
|
38
|
-
return res.json() as T
|
|
39
|
-
} catch (err) {
|
|
40
|
-
console.error('Fetch failed:', err)
|
|
41
|
-
process.exit(1)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function unflatten(flat: Record<string, any>): Record<string, any> {
|
|
46
|
-
const result: Record<string, any> = {}
|
|
47
|
-
|
|
48
|
-
for (const flatKey in flat) {
|
|
49
|
-
const parts = flatKey.split('.')
|
|
50
|
-
let current = result
|
|
51
|
-
|
|
52
|
-
parts.forEach((part, idx) => {
|
|
53
|
-
if (idx === parts.length - 1) {
|
|
54
|
-
current[part] = flat[flatKey]
|
|
55
|
-
} else {
|
|
56
|
-
if (!(part in current)) {
|
|
57
|
-
current[part] = {}
|
|
58
|
-
}
|
|
59
|
-
current = current[part]
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return result
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const DEFAULT_OPTIONS: Record<string, string> = {
|
|
68
|
-
sourcePath: '.',
|
|
69
|
-
sourceLocale: 'en-US',
|
|
70
|
-
apiOrigin: 'https://glotstack.ai',
|
|
71
|
-
yaml: 'false',
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// TODO: downcase yaml files
|
|
75
|
-
function downcaseKeys(obj: Record<string, any>): Record<string, any> {
|
|
76
|
-
return Object.fromEntries(
|
|
77
|
-
Object.entries(obj).map(([key, value]) => [key.toLowerCase(), value])
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
async function resolveConfigAndOptions(options: Record<string, any>): Promise<GlotstackConfig & { yes?: boolean; yaml?: boolean }> {
|
|
83
|
-
|
|
84
|
-
const config = await findGlotstackConfig(cwd()) ?? {}
|
|
85
|
-
const resolved = merge<GlotstackConfig>({} as GlotstackConfig, DEFAULT_OPTIONS, config, options)
|
|
86
|
-
|
|
87
|
-
// special case to match source
|
|
88
|
-
if (resolved.outputDir == null) {
|
|
89
|
-
resolved.outputDir = resolved.sourcePath
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if ('outputLocales' in options) {
|
|
93
|
-
if ((resolved.outputLocales as string[]).includes(resolved.sourceLocale)) {
|
|
94
|
-
console.warn(`${resolved.sourceLocale} detected in outputLocales, removing`)
|
|
95
|
-
options.outputLocales = options.outputLocales.filter((x: string) => x !== resolved.sourceLocale)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return resolved
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
async function run(args: string[]) {
|
|
103
|
-
program
|
|
104
|
-
.command('extract-translations')
|
|
105
|
-
.description('extract translations from all compatible source files.')
|
|
106
|
-
.option('--source-path [path]', `path directory containing [locale].json files (default=${DEFAULT_OPTIONS['sourcePath']})`)
|
|
107
|
-
.option('--source-locale [locale]', `the locale you provide "context" in, your primary locale (default=${DEFAULT_OPTIONS['sourceLocale']})`)
|
|
108
|
-
.option('--yaml', 'Use a .yaml source file and allow conversion to JSON')
|
|
109
|
-
.option('--api-origin [url]', `glotstack api origin (default=${DEFAULT_OPTIONS['apiOrigin']})`)
|
|
110
|
-
.option('--output-dir [path]', 'path to output directory (default=<source-path>')
|
|
111
|
-
.option('--api-key [key]', 'api key for glotstack.ai')
|
|
112
|
-
.option('--yes', 'skip confirm checks')
|
|
113
|
-
.argument('[directories...]', 'Directories to scan', './**/*')
|
|
114
|
-
.action(async (directories: string[], inputOptions: Record<string, any>) => {
|
|
115
|
-
const options = await resolveConfigAndOptions(inputOptions)
|
|
116
|
-
if (!options.apiOrigin) {
|
|
117
|
-
throw new Error('apiOrigin must be specified')
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const linter = new eslint.ESLint({ overrideConfigFile: path.join(__dirname, '..', 'eslint-raw-string.mjs') })
|
|
121
|
-
const results = await linter.lintFiles(directories)
|
|
122
|
-
const filesWithIssues = results
|
|
123
|
-
.filter((r) => r.errorCount + r.warningCount > 0)
|
|
124
|
-
.map((r) => r.filePath)
|
|
125
|
-
|
|
126
|
-
const rl = readline.createInterface({ input, output })
|
|
127
|
-
const askToSend = async (): Promise<boolean> => {
|
|
128
|
-
if (options.yes) {
|
|
129
|
-
return true
|
|
130
|
-
}
|
|
131
|
-
const response = await rl.question(`Your source are going to be sent to our LLM -- they should not contain any secrets. Proceed? (yes/no):`)
|
|
132
|
-
if (response === 'yes') {
|
|
133
|
-
return true
|
|
134
|
-
} else if (response !== 'no') {
|
|
135
|
-
console.error('Please respond with yes or no.')
|
|
136
|
-
return askToSend()
|
|
137
|
-
} else {
|
|
138
|
-
return false
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const send = await askToSend()
|
|
143
|
-
if (send) {
|
|
144
|
-
console.info('Sending files to generate new source and extracted strings')
|
|
145
|
-
let url = `${options.apiOrigin}/uploads/translations/extract`
|
|
146
|
-
|
|
147
|
-
if (options.yaml) {
|
|
148
|
-
url = `${url}?yaml=true`
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const form = new FormData()
|
|
152
|
-
|
|
153
|
-
for (let i = 0; i < filesWithIssues.length; i++) {
|
|
154
|
-
const filePath = filesWithIssues[i]
|
|
155
|
-
form.append(`file_${i}`, await openAsBlob(filePath), filePath)
|
|
156
|
-
console.debug(`Uploading file: ${filePath}`)
|
|
157
|
-
}
|
|
158
|
-
const data = await fetchGlotstack<{ translations: { name: string; modified_source: { url: string } }[] }>(url, options.apiKey, form)
|
|
159
|
-
data.translations.map(elem => console.info(`Source and translations available for: ${elem.name}:\n ${elem.modified_source.url}\n\n`))
|
|
160
|
-
rl.close()
|
|
161
|
-
} else {
|
|
162
|
-
rl.close()
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
program
|
|
167
|
-
.command('get-translations')
|
|
168
|
-
.description('fetch translations for all [output-locals...]. Use .glotstack.json for repeatable results.')
|
|
169
|
-
.option('--source-path [path]', `path directory containing [locale].json files (default=${DEFAULT_OPTIONS['sourcePath']})`)
|
|
170
|
-
.option('--source-locale [locale]', `the locale you provide "context" in, your primary locale (default=${DEFAULT_OPTIONS['sourceLocale']})`)
|
|
171
|
-
.option('--yaml', 'Expect to use yaml source file')
|
|
172
|
-
.option('--api-origin [url]', `glotstack api origin (default=${DEFAULT_OPTIONS['apiOrigin']})`)
|
|
173
|
-
.option('--output-dir [path]', 'path to output directory (default=<source-path>')
|
|
174
|
-
.option('--api-key [key]', 'api key for glotstack.ai')
|
|
175
|
-
.option('--project-id [id]', '(optional) specific project to use')
|
|
176
|
-
.option('--only [locale]', '(optional) only translate for this locale')
|
|
177
|
-
.argument('[output-locales...]', 'locales to get translations for')
|
|
178
|
-
.action(async (outputLocales: string[], options: Record<string, any>, command: Command) => {
|
|
179
|
-
const resolved = await resolveConfigAndOptions({ ...options, outputLocales: outputLocales })
|
|
180
|
-
if (!resolved.sourcePath) {
|
|
181
|
-
throw new Error('sourcePath must be specified')
|
|
182
|
-
}
|
|
183
|
-
if (!resolved.apiOrigin) {
|
|
184
|
-
throw new Error('apiOrigin must be specified')
|
|
185
|
-
}
|
|
186
|
-
if (!resolved.outputDir) {
|
|
187
|
-
throw new Error('outputDir must be specified')
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const ext = options.yaml == true ? '.yaml' : '.json'
|
|
191
|
-
const absPath = path.resolve(resolved.sourcePath, `${resolved.sourceLocale}${ext}`)
|
|
192
|
-
const fileContent = await fs.readFile(absPath, 'utf-8')
|
|
193
|
-
|
|
194
|
-
let json = null
|
|
195
|
-
try {
|
|
196
|
-
json = loadYaml(fileContent)
|
|
197
|
-
} catch (err) {
|
|
198
|
-
try {
|
|
199
|
-
json = JSON.parse(fileContent)
|
|
200
|
-
} catch (err) {
|
|
201
|
-
console.error('Unable to parse source file ', absPath, err)
|
|
202
|
-
throw err
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
let locales: string[] = (resolved.outputLocales as string[]).map(l => l)
|
|
206
|
-
if (options.only != null) {
|
|
207
|
-
locales = locales.filter(l => l === options.only)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const body = {
|
|
211
|
-
locales,
|
|
212
|
-
translations: json,
|
|
213
|
-
usage: options.usage,
|
|
214
|
-
...{ ... (resolved.projectId != null ? { projectId: resolved.projectId } : {}) },
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const url = `${resolved.apiOrigin}/api/translations`
|
|
218
|
-
console.info('Getting translations for: ', locales)
|
|
219
|
-
const data = await fetchGlotstack<{ data: Translations }>(url, resolved.apiKey, body)
|
|
220
|
-
console.info('Received translations:', data)
|
|
221
|
-
Object.entries(data.data).map(([key, val]) => {
|
|
222
|
-
const p = `${resolved.outputDir}/${key}.json`
|
|
223
|
-
console.info(`Writing file ${p}`)
|
|
224
|
-
fs.writeFile(`${resolved.outputDir}/${key}.json`, JSON.stringify(val, null, 2), 'utf-8')
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
if (options.yaml) {
|
|
228
|
-
const fp = `${resolved.outputDir}/${path.parse(absPath).name}.json`
|
|
229
|
-
console.info(`Writing file ${fp}`)
|
|
230
|
-
fs.writeFile(fp, JSON.stringify(json, null, 2), 'utf-8')
|
|
231
|
-
}
|
|
232
|
-
})
|
|
233
|
-
|
|
234
|
-
program
|
|
235
|
-
.command('yaml-to-json')
|
|
236
|
-
.option('--source-path [path]', `path directory containing [locale].json files (default=${DEFAULT_OPTIONS['sourcePath']})`)
|
|
237
|
-
.action(async (inputOptions: Record<string, any>) => {
|
|
238
|
-
const options = await resolveConfigAndOptions(inputOptions)
|
|
239
|
-
|
|
240
|
-
const absPath = path.resolve(options.sourcePath, `${options.sourceLocale}.yaml`)
|
|
241
|
-
const fileContent = await fs.readFile(absPath, 'utf-8')
|
|
242
|
-
const fp = `${options.outputDir}/${path.parse(absPath).name}.json`
|
|
243
|
-
const json = loadYaml(fileContent)
|
|
244
|
-
console.info(`Writing file ${fp}`)
|
|
245
|
-
fs.writeFile(fp, JSON.stringify(json, null, 2), 'utf-8')
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
program
|
|
250
|
-
.command('format-json')
|
|
251
|
-
.description('format files in --source-path [path] to nested (not flat)')
|
|
252
|
-
.option('--source-path [path]', `path directory containing [locale].json files (default=${DEFAULT_OPTIONS['sourcePath']})`)
|
|
253
|
-
.option('--yes', 'skip confirm checks')
|
|
254
|
-
.action(async (inputOptions: Record<string, any>) => {
|
|
255
|
-
const options = await resolveConfigAndOptions(inputOptions)
|
|
256
|
-
|
|
257
|
-
if (!options.sourcePath) {
|
|
258
|
-
throw new Error('sourcePath must be specified')
|
|
259
|
-
}
|
|
260
|
-
const rl = readline.createInterface({ input, output })
|
|
261
|
-
const askToSend = async (): Promise<boolean> => {
|
|
262
|
-
if (options.yes) {
|
|
263
|
-
return true
|
|
264
|
-
}
|
|
265
|
-
const response = await rl.question(`This will update your source files -- have you checked them into SCM/git? Type yes to proceed (yes/no):`)
|
|
266
|
-
if (response === 'yes') {
|
|
267
|
-
return true
|
|
268
|
-
} else if (response !== 'no') {
|
|
269
|
-
console.error('Please respond with yes or no.')
|
|
270
|
-
return askToSend()
|
|
271
|
-
} else {
|
|
272
|
-
return false
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const yes = await askToSend()
|
|
276
|
-
if (yes) {
|
|
277
|
-
const files = await readdir(options.sourcePath)
|
|
278
|
-
for (let i = 0; i < (await files).length; i++) {
|
|
279
|
-
const fp = resolve(options.sourcePath, files[i])
|
|
280
|
-
const text = await readFile(fp, 'utf-8')
|
|
281
|
-
const json = JSON.parse(text)
|
|
282
|
-
const formatted = JSON.stringify(unflatten(json), null, 2)
|
|
283
|
-
await writeFile(fp, formatted, 'utf-8')
|
|
284
|
-
}
|
|
285
|
-
rl.close()
|
|
286
|
-
}
|
|
287
|
-
rl.close()
|
|
288
|
-
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
await program.parseAsync(args)
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
run(process.argv)
|