glotstack 0.0.6 → 0.0.8
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 +145 -54
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +14 -4
- package/dist/index.js +63 -19
- package/dist/index.js.map +1 -1
- package/dist/util/findConfig.d.ts +1 -1
- package/dist/util/findConfig.js +40 -13
- package/dist/util/findConfig.js.map +1 -1
- package/eslint-raw-string.mjs +5 -1
- package/package.json +13 -8
- package/src/cli.tsx +150 -50
- package/src/index.tsx +70 -29
- package/src/util/findConfig.ts +27 -11
- package/tsconfig.json +1 -1
- package/package-lock.json +0 -4356
package/package.json
CHANGED
|
@@ -1,36 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glotstack",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
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
|
+
"@eslint/js": "^9.26.0",
|
|
10
11
|
"commander": "^13.1.0",
|
|
11
|
-
"eslint": "^9.26.0"
|
|
12
|
+
"eslint": "^9.26.0",
|
|
13
|
+
"eslint-plugin-i18next": "^6.1.1",
|
|
14
|
+
"eslint-plugin-react": "^7.37.5",
|
|
15
|
+
"form-data": "^4.0.2",
|
|
16
|
+
"undici": "^7.9.0"
|
|
12
17
|
},
|
|
13
18
|
"devDependencies": {
|
|
14
|
-
"@eslint/js": "^9.26.0",
|
|
15
19
|
"@types/js-yaml": "^4.0.9",
|
|
16
20
|
"@types/node": "^22.15.17",
|
|
17
|
-
"@types/react": "^
|
|
21
|
+
"@types/react": "^18.3.1",
|
|
22
|
+
"@types/react-dom": "^18.3.1",
|
|
18
23
|
"globals": "^16.1.0",
|
|
19
24
|
"js-yaml": "^4.1.0",
|
|
25
|
+
"nodemon": "^3.1.10",
|
|
20
26
|
"typescript": "5.4.4",
|
|
21
27
|
"typescript-eslint": "^8.32.1"
|
|
22
28
|
},
|
|
23
29
|
"peerDependencies": {
|
|
24
|
-
"react": "^
|
|
25
|
-
|
|
26
|
-
"resolutions": {
|
|
27
|
-
"react": "18.3.1"
|
|
30
|
+
"react": "^18.3.1",
|
|
31
|
+
"react-dom": "^18.3.1"
|
|
28
32
|
},
|
|
29
33
|
"bin": {
|
|
30
34
|
"glotstack": "dist/cli.js"
|
|
31
35
|
},
|
|
32
36
|
"scripts": {
|
|
33
37
|
"build": "tsc && scripts/fix-shebang.sh dist/cli.js",
|
|
38
|
+
"watch": "nodemon --watch src --ext ts,tsx,mjs,json --exec \"bash -c 'npm run build'\"",
|
|
34
39
|
"prepublishOnly": "yarn run build",
|
|
35
40
|
"glotstack": "node dist/cli.js"
|
|
36
41
|
}
|
package/src/cli.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Command, program } from 'commander'
|
|
2
2
|
import path from 'path'
|
|
3
|
-
import { promises as fs,
|
|
3
|
+
import { promises as fs, createReadStream } from 'fs'
|
|
4
4
|
import { findGlotstackConfig } from './util/findConfig'
|
|
5
5
|
import { cwd } from 'process'
|
|
6
6
|
import { merge } from './util/object'
|
|
@@ -8,36 +8,102 @@ import { loadYaml } from './util/yaml'
|
|
|
8
8
|
import eslint from 'eslint'
|
|
9
9
|
import * as readline from "node:readline/promises"
|
|
10
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
|
+
}
|
|
11
25
|
|
|
26
|
+
let payloadBody: FormData | string
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'Content-Type': 'application/json',
|
|
19
|
-
'Authorization': `Bearer ${apiKey}`,
|
|
28
|
+
if (!(body instanceof FormData)) {
|
|
29
|
+
headers['content-type'] = 'application/json'
|
|
30
|
+
payloadBody = JSON.stringify(body)
|
|
31
|
+
} else {
|
|
32
|
+
payloadBody = body
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
try {
|
|
23
|
-
const res = await fetch(url, { method: 'POST', body:
|
|
36
|
+
const res = await fetch(url, { method: 'POST', body: payloadBody, headers })
|
|
24
37
|
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))
|
|
38
|
+
return res.json() as T
|
|
27
39
|
} catch (err) {
|
|
28
40
|
console.error('Fetch failed:', err)
|
|
29
41
|
process.exit(1)
|
|
30
42
|
}
|
|
31
43
|
}
|
|
32
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
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveConfigAndOptions(options: Record<string, any>) {
|
|
74
|
+
|
|
75
|
+
const config = await findGlotstackConfig(cwd()) ?? {}
|
|
76
|
+
|
|
77
|
+
if ('outputLocales' in options) {
|
|
78
|
+
if ((options.outputLocales as string[]).includes('en-US')) {
|
|
79
|
+
console.warn('en-US detected in outputLocales, removing')
|
|
80
|
+
options.outputLocales = options.outputLocales.filter((x: string) => x !== 'en-US')
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolved = merge({}, DEFAULT_OPTIONS, config, options)
|
|
85
|
+
|
|
86
|
+
// special case to match source
|
|
87
|
+
if (resolved.outputDir == null) {
|
|
88
|
+
resolved.outputDir = resolved.sourcePath
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return resolved
|
|
92
|
+
}
|
|
93
|
+
|
|
33
94
|
|
|
34
95
|
async function run(args: string[]) {
|
|
35
96
|
program
|
|
36
97
|
.command('extract-translations')
|
|
37
|
-
.
|
|
38
|
-
.option('--
|
|
39
|
-
.option('--
|
|
40
|
-
.
|
|
98
|
+
.description('extract translations from all compatible source files.')
|
|
99
|
+
.option('--source-path [path]', `path directory containing [locale].json files (default=${DEFAULT_OPTIONS['sourcePath']})`)
|
|
100
|
+
.option('--source-locale [locale]', `the locale you provide "context" in, your primary locale (default=${DEFAULT_OPTIONS['sourceLocale']})`)
|
|
101
|
+
.option('--api-origin [url]', `glotstack api origin (default=${DEFAULT_OPTIONS['apiOrigin']})`)
|
|
102
|
+
.option('--output-dir [path]', 'path to output directory (default=<source-path>')
|
|
103
|
+
.option('--api-key [key]', 'api key for glotstack.ai')
|
|
104
|
+
.option('--yes', 'skip confirm checks')
|
|
105
|
+
.action(async (inputOptions: Record<string, any>) => {
|
|
106
|
+
const options = await resolveConfigAndOptions(inputOptions)
|
|
41
107
|
if (!options.apiOrigin) {
|
|
42
108
|
throw new Error('apiOrigin must be specified')
|
|
43
109
|
}
|
|
@@ -49,7 +115,6 @@ async function run(args: string[]) {
|
|
|
49
115
|
.map((r) => r.filePath)
|
|
50
116
|
|
|
51
117
|
const rl = readline.createInterface({ input, output })
|
|
52
|
-
|
|
53
118
|
const askToSend = async (): Promise<boolean> => {
|
|
54
119
|
if (options.yes) {
|
|
55
120
|
return true
|
|
@@ -67,7 +132,17 @@ async function run(args: string[]) {
|
|
|
67
132
|
|
|
68
133
|
const send = await askToSend()
|
|
69
134
|
if (send) {
|
|
70
|
-
console.info('Sending files to
|
|
135
|
+
console.info('Sending files to generate new source and extracted strings')
|
|
136
|
+
const url = `${options.apiOrigin}/uploads/translations/extract`
|
|
137
|
+
const form = new FormData()
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < filesWithIssues.length; i++) {
|
|
140
|
+
const filePath = filesWithIssues[i]
|
|
141
|
+
form.append(`file_${i}`, await openAsBlob(filePath), filePath)
|
|
142
|
+
console.debug(`Uploading file: ${filePath}`)
|
|
143
|
+
}
|
|
144
|
+
const data = await fetchGlotstack<{ translations: { name: string; modified_source: { url: string } }[] }>(url, options.apiKey, form)
|
|
145
|
+
data.translations.map(elem => console.info(`Source and translations available for: ${elem.name}:\n ${elem.modified_source.url}\n\n`))
|
|
71
146
|
rl.close()
|
|
72
147
|
} else {
|
|
73
148
|
rl.close()
|
|
@@ -76,46 +151,27 @@ async function run(args: string[]) {
|
|
|
76
151
|
|
|
77
152
|
program
|
|
78
153
|
.command('get-translations')
|
|
79
|
-
.
|
|
80
|
-
.option('--
|
|
81
|
-
.option('--
|
|
154
|
+
.description('fetch translations for all [output-locals...]. Use .glotstack.json for repeatable results.')
|
|
155
|
+
.option('--source-path [path]', `path directory containing [locale].json files (default=${DEFAULT_OPTIONS['sourcePath']})`)
|
|
156
|
+
.option('--source-locale [locale]', `the locale you provide "context" in, your primary locale (default=${DEFAULT_OPTIONS['sourceLocale']})`)
|
|
157
|
+
.option('--api-origin [url]', `glotstack api origin (default=${DEFAULT_OPTIONS['apiOrigin']})`)
|
|
158
|
+
.option('--output-dir [path]', 'path to output directory (default=<source-path>')
|
|
82
159
|
.option('--api-key [key]', 'api key for glotstack.ai')
|
|
83
160
|
.option('--project-id [id]', '(optional) specific project to use')
|
|
84
161
|
.argument('[output-locales...]', 'locales to get translations for')
|
|
85
162
|
.action(async (outputLocales: string[], options: Record<string, any>, command: Command) => {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (configPath != null) {
|
|
90
|
-
console.info('Loading config file at ', configPath)
|
|
91
|
-
try {
|
|
92
|
-
const text = await fs.readFile(configPath, 'utf-8')
|
|
93
|
-
config = JSON.parse(text)
|
|
94
|
-
console.info('Loaded config file', config)
|
|
95
|
-
} catch (err) {
|
|
96
|
-
//pass
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const resolved = merge(config, options, { outputLocales })
|
|
101
|
-
const { apiOrigin, sourcePath, outputDir, projectId } = resolved
|
|
102
|
-
|
|
103
|
-
if ((resolved.outputLocales as string[]).includes('en-US')) {
|
|
104
|
-
console.warn('en-US detected in outputLocales, removing')
|
|
105
|
-
resolved.outputLocales = resolved.outputLocales.filter((x: string) => x !== 'en-US')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!sourcePath) {
|
|
163
|
+
const resolved = await resolveConfigAndOptions({ ...options, outputLocales: outputLocales })
|
|
164
|
+
if (!resolved.sourcePath) {
|
|
109
165
|
throw new Error('sourcePath must be specified')
|
|
110
166
|
}
|
|
111
|
-
if (!apiOrigin) {
|
|
167
|
+
if (!resolved.apiOrigin) {
|
|
112
168
|
throw new Error('apiOrigin must be specified')
|
|
113
169
|
}
|
|
114
|
-
if (!outputDir) {
|
|
170
|
+
if (!resolved.outputDir) {
|
|
115
171
|
throw new Error('outputDir must be specified')
|
|
116
172
|
}
|
|
117
173
|
|
|
118
|
-
const absPath = path.resolve(sourcePath)
|
|
174
|
+
const absPath = path.resolve(resolved.sourcePath, `${resolved.sourceLocale}.json`)
|
|
119
175
|
const fileContent = await fs.readFile(absPath, 'utf-8')
|
|
120
176
|
|
|
121
177
|
let json = null
|
|
@@ -133,18 +189,62 @@ async function run(args: string[]) {
|
|
|
133
189
|
const body = {
|
|
134
190
|
locales: resolved.outputLocales,
|
|
135
191
|
translations: json,
|
|
136
|
-
...{ ... (projectId != null ? { projectId } : {}) },
|
|
192
|
+
...{ ... (resolved.projectId != null ? { projectId: resolved.projectId } : {}) },
|
|
137
193
|
}
|
|
138
194
|
|
|
139
|
-
const
|
|
195
|
+
const url = `${resolved.apiOrigin}/api/translations`
|
|
196
|
+
const data = await fetchGlotstack<{ data: Translations }>(url, resolved.apiKey, body)
|
|
140
197
|
console.info('Received translations:', data)
|
|
141
198
|
Object.entries(data.data).map(([key, val]) => {
|
|
142
|
-
const p = `${outputDir}/${key}.json`
|
|
199
|
+
const p = `${resolved.outputDir}/${key}.json`
|
|
143
200
|
console.info(`Writing file ${p}`)
|
|
144
|
-
fs.writeFile(`${outputDir}/${key}.json`, JSON.stringify(val, null, 2))
|
|
201
|
+
fs.writeFile(`${resolved.outputDir}/${key}.json`, JSON.stringify(val, null, 2))
|
|
145
202
|
})
|
|
146
203
|
})
|
|
147
204
|
|
|
205
|
+
program
|
|
206
|
+
.command('format-json')
|
|
207
|
+
.description('format files in --source-path [path] to nested (not flat)')
|
|
208
|
+
.option('--source-path [path]', `path directory containing [locale].json files (default=${DEFAULT_OPTIONS['sourcePath']})`)
|
|
209
|
+
.option('--yes', 'skip confirm checks')
|
|
210
|
+
.action(async (inputOptions: Record<string, any>) => {
|
|
211
|
+
const options = await resolveConfigAndOptions(inputOptions)
|
|
212
|
+
|
|
213
|
+
if (!options.sourcePath) {
|
|
214
|
+
throw new Error('sourcePath must be specified')
|
|
215
|
+
}
|
|
216
|
+
const rl = readline.createInterface({ input, output })
|
|
217
|
+
const askToSend = async (): Promise<boolean> => {
|
|
218
|
+
if (options.yes) {
|
|
219
|
+
return true
|
|
220
|
+
}
|
|
221
|
+
const response = await rl.question(`This will update your source files -- have you checked them into SCM/git? Type yes to proceed (yes/no):`)
|
|
222
|
+
if (response === 'yes') {
|
|
223
|
+
return true
|
|
224
|
+
} else if (response !== 'no') {
|
|
225
|
+
console.error('Please respond with yes or no.')
|
|
226
|
+
return askToSend()
|
|
227
|
+
} else {
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const yes = await askToSend()
|
|
232
|
+
if (yes) {
|
|
233
|
+
const files = await readdir(options.sourcePath)
|
|
234
|
+
for (let i = 0; i < (await files).length; i++) {
|
|
235
|
+
const fp = resolve(options.sourcePath, files[i])
|
|
236
|
+
const text = await readFile(fp, 'utf-8')
|
|
237
|
+
const json = JSON.parse(text)
|
|
238
|
+
const formatted = JSON.stringify(unflatten(json), null, 2)
|
|
239
|
+
await writeFile(fp, formatted)
|
|
240
|
+
}
|
|
241
|
+
rl.close()
|
|
242
|
+
}
|
|
243
|
+
rl.close()
|
|
244
|
+
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
|
|
148
248
|
await program.parseAsync(args)
|
|
149
249
|
}
|
|
150
250
|
|
package/src/index.tsx
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
|
-
import { merge
|
|
2
|
+
import { merge } from './util/object'
|
|
3
|
+
import { debug } from 'node:console'
|
|
3
4
|
|
|
4
|
-
export type LocaleRegion =
|
|
5
|
-
| 'en'
|
|
6
|
-
| 'en-US'
|
|
7
|
-
| 'en-US-genz'
|
|
8
|
-
| 'fr-FR'
|
|
9
|
-
| 'de-DE'
|
|
10
|
-
| 'nl-NL'
|
|
11
|
-
| 'jp-JP'
|
|
12
|
-
| string
|
|
5
|
+
export type LocaleRegion = string
|
|
13
6
|
|
|
14
7
|
|
|
15
8
|
export interface TranslationLeaf {
|
|
@@ -21,13 +14,15 @@ export interface Translations {
|
|
|
21
14
|
[key: string]: Translations | TranslationLeaf
|
|
22
15
|
}
|
|
23
16
|
|
|
17
|
+
|
|
24
18
|
export interface ContextType {
|
|
25
19
|
translations: Translations
|
|
26
20
|
locale: string | null
|
|
27
21
|
loadTranslations: (locale: LocaleRegion) => Promise<Translations>
|
|
28
22
|
setLocale: (locale: LocaleRegion) => void
|
|
29
|
-
importMethod: (locale: LocaleRegion) => Promise<Translations>
|
|
23
|
+
importMethod: (locale: LocaleRegion) => Promise<Translations>
|
|
30
24
|
t: (key: string, options?: { locale?: LocaleRegion }) => string
|
|
25
|
+
ssr: boolean
|
|
31
26
|
}
|
|
32
27
|
|
|
33
28
|
export const GlotstackContext = React.createContext<ContextType>({
|
|
@@ -37,18 +32,61 @@ export const GlotstackContext = React.createContext<ContextType>({
|
|
|
37
32
|
locale: null,
|
|
38
33
|
importMethod: (_locale: LocaleRegion) => { throw new Error('import method not set') },
|
|
39
34
|
t: () => { throw new Error('import method not set') },
|
|
35
|
+
ssr: false
|
|
40
36
|
})
|
|
41
37
|
|
|
42
38
|
interface GlotstackProviderProps {
|
|
43
39
|
children: React.ReactNode
|
|
44
|
-
initialTranslations?: Translations
|
|
40
|
+
initialTranslations?: Record<string, Translations>
|
|
45
41
|
initialLocale?: LocaleRegion
|
|
46
42
|
onTranslationLoaded?: (locale: LocaleRegion, translations: Translations) => void
|
|
47
43
|
onLocaleChange?: (locale: LocaleRegion) => void
|
|
48
|
-
importMethod:
|
|
44
|
+
importMethod: ContextType['importMethod']
|
|
45
|
+
ssr?: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export enum LogLevel {
|
|
49
|
+
DEBUG = 0,
|
|
50
|
+
LOG = 1,
|
|
51
|
+
INFO = 2,
|
|
52
|
+
WARNING = 3,
|
|
53
|
+
ERROR = 4,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const LogLevelToFunc: Record<LogLevel, (...args: Parameters<typeof console.info>) => void> = {
|
|
57
|
+
[LogLevel.DEBUG]: console.debug,
|
|
58
|
+
[LogLevel.INFO]: console.info,
|
|
59
|
+
[LogLevel.LOG]: console.log,
|
|
60
|
+
[LogLevel.WARNING]: console.warn,
|
|
61
|
+
[LogLevel.ERROR]: console.error,
|
|
62
|
+
} as const
|
|
63
|
+
|
|
64
|
+
let logLevel: LogLevel = LogLevel.DEBUG
|
|
65
|
+
|
|
66
|
+
export const setLogLevel = (level: LogLevel) => {
|
|
67
|
+
logLevel = level
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const makeLoggingFunction = (level: LogLevel) => (...args: Parameters<typeof console.info>) => {
|
|
71
|
+
const func = LogLevelToFunc[level]
|
|
72
|
+
if (level < logLevel) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
return func(`[level=${level} logLevel=${logLevel}][glotstack.ai]`, ...args)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const logger = {
|
|
79
|
+
debug: makeLoggingFunction(LogLevel.DEBUG),
|
|
80
|
+
info: makeLoggingFunction(LogLevel.INFO),
|
|
81
|
+
warn: makeLoggingFunction(LogLevel.WARNING),
|
|
82
|
+
error: makeLoggingFunction(LogLevel.ERROR),
|
|
83
|
+
|
|
49
84
|
}
|
|
50
85
|
|
|
51
86
|
export const access = (key: string, locale: LocaleRegion, translations: Translations) => {
|
|
87
|
+
if (translations == null) {
|
|
88
|
+
return key
|
|
89
|
+
}
|
|
52
90
|
const access = [...key.split('.')] as [LocaleRegion, ...string[]]
|
|
53
91
|
const localeTranslations = translations?.[locale]
|
|
54
92
|
|
|
@@ -64,21 +102,29 @@ export const access = (key: string, locale: LocaleRegion, translations: Translat
|
|
|
64
102
|
return (value?.value ?? key) as string
|
|
65
103
|
}
|
|
66
104
|
|
|
67
|
-
export const GlotstackProvider = ({ children, initialLocale, initialTranslations, onLocaleChange, onTranslationLoaded, importMethod }: GlotstackProviderProps) => {
|
|
105
|
+
export const GlotstackProvider = ({ children, initialLocale, initialTranslations, onLocaleChange, onTranslationLoaded, importMethod, ssr}: GlotstackProviderProps) => {
|
|
68
106
|
if (initialLocale == null) {
|
|
69
107
|
throw new Error('initialLocale must be set')
|
|
70
108
|
}
|
|
71
109
|
const [locale, setLocale] = React.useState<LocaleRegion>(initialLocale)
|
|
72
|
-
const translationsRef = React.useRef(initialTranslations)
|
|
110
|
+
const translationsRef = React.useRef<Record<string, Translations>|null>(initialTranslations || null)
|
|
73
111
|
const loadingRef = React.useRef<Record<string, Promise<Translations>>>({})
|
|
74
112
|
|
|
75
|
-
const loadTranslations = React.useCallback(async (locale: string) => {
|
|
113
|
+
const loadTranslations = React.useCallback(async (locale: string, opts?: {force?: boolean}) => {
|
|
76
114
|
// TODO: if translations are loaded only reload if some condition is
|
|
77
115
|
try {
|
|
78
|
-
if (loadingRef.current?.[locale] != null) {
|
|
116
|
+
if (loadingRef.current?.[locale] != null && opts?.force != true) {
|
|
117
|
+
logger.debug('Waiting for translations already loading', locale)
|
|
79
118
|
return (await loadingRef.current?.[locale])
|
|
80
119
|
}
|
|
81
|
-
|
|
120
|
+
if (translationsRef.current?.[locale] != null && opts?.force != true) {
|
|
121
|
+
logger.debug('Skipping load for translations', locale, translationsRef.current?.[locale], translationsRef.current)
|
|
122
|
+
return translationsRef.current?.[locale]
|
|
123
|
+
}
|
|
124
|
+
if (loadingRef.current != null) {
|
|
125
|
+
logger.debug('Loading translations', locale, merge({}, translationsRef.current ?? {}))
|
|
126
|
+
loadingRef.current[locale] = importMethod(locale)
|
|
127
|
+
}
|
|
82
128
|
const result = await loadingRef.current[locale]
|
|
83
129
|
|
|
84
130
|
if (result == null) {
|
|
@@ -90,7 +136,7 @@ export const GlotstackProvider = ({ children, initialLocale, initialTranslations
|
|
|
90
136
|
onTranslationLoaded?.(locale, result)
|
|
91
137
|
return result
|
|
92
138
|
} catch (err) {
|
|
93
|
-
|
|
139
|
+
logger.error('Unable to import translations', err)
|
|
94
140
|
throw err
|
|
95
141
|
}
|
|
96
142
|
}, [importMethod, onTranslationLoaded])
|
|
@@ -98,7 +144,7 @@ export const GlotstackProvider = ({ children, initialLocale, initialTranslations
|
|
|
98
144
|
React.useEffect(() => {
|
|
99
145
|
const run = async () => {
|
|
100
146
|
onLocaleChange?.(locale)
|
|
101
|
-
|
|
147
|
+
await loadTranslations(locale)
|
|
102
148
|
}
|
|
103
149
|
React.startTransition(() => {
|
|
104
150
|
run()
|
|
@@ -113,15 +159,11 @@ export const GlotstackProvider = ({ children, initialLocale, initialTranslations
|
|
|
113
159
|
importMethod,
|
|
114
160
|
loadTranslations,
|
|
115
161
|
t: (key: string, opts?: { locale?: LocaleRegion }) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
loadTranslations(opts?.locale)
|
|
121
|
-
}, [locale, opts?.locale])
|
|
162
|
+
const resolvedLocale = opts?.locale ?? locale
|
|
163
|
+
loadTranslations(resolvedLocale)
|
|
122
164
|
return access(key, opts?.locale ?? locale, translationsRef.current ?? {})
|
|
123
|
-
}
|
|
124
|
-
|
|
165
|
+
},
|
|
166
|
+
ssr: ssr == true,
|
|
125
167
|
}
|
|
126
168
|
}, [locale, importMethod])
|
|
127
169
|
|
|
@@ -134,7 +176,6 @@ export const useGlotstack = () => {
|
|
|
134
176
|
return React.useContext(GlotstackContext)
|
|
135
177
|
}
|
|
136
178
|
|
|
137
|
-
|
|
138
179
|
export const useTranslations = (_options?: Record<never, never>) => {
|
|
139
180
|
const context = React.useContext(GlotstackContext)
|
|
140
181
|
return context
|
package/src/util/findConfig.ts
CHANGED
|
@@ -1,26 +1,42 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { existsSync } from 'fs'
|
|
2
|
+
import { readFile } from 'fs/promises'
|
|
3
|
+
import * as path from 'path'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Recursively looks for `.glotstack.json` from the current directory up to the root.
|
|
6
7
|
* @param startDir The directory to start the search from. Defaults to process.cwd().
|
|
7
8
|
* @returns The absolute path to the file if found, or null if not found.
|
|
8
9
|
*/
|
|
9
|
-
export function findGlotstackConfig(startDir: string = process.cwd()):
|
|
10
|
-
let currentDir = path.resolve(startDir)
|
|
10
|
+
export async function findGlotstackConfig(startDir: string = process.cwd()): Promise<object | null> {
|
|
11
|
+
let currentDir = path.resolve(startDir)
|
|
12
|
+
let configPath = null
|
|
11
13
|
|
|
12
14
|
while (true) {
|
|
13
|
-
const candidate = path.join(currentDir, '.glotstack.json')
|
|
14
|
-
if (
|
|
15
|
-
|
|
15
|
+
const candidate = path.join(currentDir, '.glotstack.json')
|
|
16
|
+
if (existsSync(candidate)) {
|
|
17
|
+
configPath = candidate
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
const parentDir = path.dirname(currentDir)
|
|
20
|
+
const parentDir = path.dirname(currentDir)
|
|
19
21
|
if (parentDir === currentDir) {
|
|
20
|
-
break
|
|
22
|
+
break // Reached root
|
|
21
23
|
}
|
|
22
|
-
currentDir = parentDir
|
|
24
|
+
currentDir = parentDir
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
|
|
27
|
+
let config = {}
|
|
28
|
+
|
|
29
|
+
if (configPath != null) {
|
|
30
|
+
console.info('Loading config file at ', configPath)
|
|
31
|
+
try {
|
|
32
|
+
const text = await readFile(configPath, 'utf-8')
|
|
33
|
+
config = JSON.parse(text)
|
|
34
|
+
console.info('Loaded config file', config)
|
|
35
|
+
return config
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.warn('Could not load config', configPath)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
console.warn('Could not find any .glotstack.json config files')
|
|
41
|
+
return null
|
|
26
42
|
}
|