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/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "glotstack",
3
- "version": "0.0.11",
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": "^18.3.1",
22
- "@types/react-dom": "^18.3.1",
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": "^18.3.1",
31
- "react-dom": "^18.3.1"
34
+ "react": ">=19",
35
+ "react-dom": ">=19",
36
+ "react-native": ">=0.81"
32
37
  },
33
- "bin": {
34
- "glotstack": "dist/cli.js"
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
@@ -1,10 +0,0 @@
1
- {
2
- "tabWidth": 2,
3
- "useTabs": false,
4
- "semi": false,
5
- "singleQuote": true,
6
- "jsxSingleQuote": true,
7
- "jsxBracketSameLine": false,
8
- "quoteProps": "as-needed",
9
- "printWidth": 80
10
- }
@@ -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
- ]);
@@ -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)