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/package.json CHANGED
@@ -1,25 +1,27 @@
1
1
  {
2
2
  "name": "glotstack",
3
- "version": "0.0.4",
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.3",
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.17.0"
21
+ "typescript-eslint": "^8.32.1"
19
22
  },
20
23
  "peerDependencies": {
21
- "@types/react": "^19.1.3",
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', 'http://localhost:4001')
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 [translations, setTranslations] = React.useState(initialTranslations)
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
- setTranslations({ ...translations, [locale]: result })
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, translations, onTranslationLoaded, setTranslations])
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: 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, translations ?? {})
121
+ }, [locale, opts?.locale])
122
+ return access(key, opts?.locale ?? locale, translationsRef.current ?? {})
121
123
  }
122
124
 
123
125
  }
124
- }, [locale, importMethod, translations])
126
+ }, [locale, importMethod])
125
127
 
126
128
  return <GlotstackContext.Provider value={context}>
127
129
  {children}