glotstack 0.0.8 → 0.0.10
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 +49 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +28 -6
- package/dist/index.js +195 -14
- package/dist/index.js.map +1 -1
- package/dist/util/findConfig.d.ts +10 -1
- package/dist/util/findConfig.js +45 -7
- package/dist/util/findConfig.js.map +1 -1
- package/dist/util/object.d.ts +1 -3
- package/dist/util/object.js +1 -3
- package/dist/util/object.js.map +1 -1
- package/package.json +3 -3
- package/src/cli.tsx +62 -18
- package/src/index.tsx +353 -18
- package/src/util/findConfig.ts +62 -8
- package/src/util/object.ts +2 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findConfig.js","sourceRoot":"","sources":["../../src/util/findConfig.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2BAA+B;AAC/B,0CAAsC;AACtC,2CAA4B;
|
|
1
|
+
{"version":3,"file":"findConfig.js","sourceRoot":"","sources":["../../src/util/findConfig.ts"],"names":[],"mappings":"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"}
|
package/dist/util/object.d.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
export declare function isObject(val: any): val is Record<string, any>;
|
|
2
|
-
|
|
3
|
-
export declare function merge(target: MergeTarget, ...sources: MergeTarget[]): MergeTarget;
|
|
2
|
+
export declare function merge<T extends object>(target: T, ...sources: Partial<T>[]): T;
|
|
4
3
|
export declare function isEqual(a: any, b: any): boolean;
|
|
5
|
-
export {};
|
package/dist/util/object.js
CHANGED
|
@@ -24,9 +24,7 @@ function _merge(target, source) {
|
|
|
24
24
|
return target;
|
|
25
25
|
}
|
|
26
26
|
function merge(target, ...sources) {
|
|
27
|
-
return sources.reduce((previous, current) =>
|
|
28
|
-
return _merge(previous, current);
|
|
29
|
-
}, target);
|
|
27
|
+
return sources.reduce((previous, current) => _merge(previous, current), target);
|
|
30
28
|
}
|
|
31
29
|
exports.merge = merge;
|
|
32
30
|
function isEqual(a, b) {
|
package/dist/util/object.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"object.js","sourceRoot":"","sources":["../../src/util/object.ts"],"names":[],"mappings":";;;AAAA,SAAgB,QAAQ,CAAC,GAAQ;IAC/B,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AACxE,CAAC;AAFD,4BAEC;AAED,SAAS,MAAM,CAAC,MAAW,EAAE,MAAW;IACtC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAE1D,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAE3B,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACnD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtC,CAAC;aAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAChD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;QACvB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"object.js","sourceRoot":"","sources":["../../src/util/object.ts"],"names":[],"mappings":";;;AAAA,SAAgB,QAAQ,CAAC,GAAQ;IAC/B,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AACxE,CAAC;AAFD,4BAEC;AAED,SAAS,MAAM,CAAC,MAAW,EAAE,MAAW;IACtC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IAE1D,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QACtC,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAE3B,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACnD,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACtC,CAAC;aAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAChD,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;QACvB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAiB,KAAK,CAAmB,MAAS,EAAE,GAAG,OAAqB;IAC1E,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,EAAE,MAAM,CAAM,CAAA;AACtF,CAAC;AAFD,sBAEC;AAED,SAAgB,OAAO,CAAC,CAAM,EAAE,CAAM;IACpC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEzB,IAAI,OAAO,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,KAAK,CAAC;IAElE,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAC7D,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7B,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,KAAK,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM;YAAE,OAAO,KAAK,CAAC;QAEhD,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAnBD,0BAmBC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glotstack",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"author": "JD Cumpson",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "tsc && scripts/fix-shebang.sh dist/cli.js",
|
|
38
|
-
"watch": "nodemon --watch src --ext ts,tsx,mjs,json --exec \"bash -c '
|
|
39
|
-
"prepublishOnly": "yarn
|
|
38
|
+
"watch": "nodemon --watch src --ext ts,tsx,mjs,json --exec \"bash -c 'yarn build'\"",
|
|
39
|
+
"prepublishOnly": "yarn build",
|
|
40
40
|
"glotstack": "node dist/cli.js"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/cli.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Command, program } from 'commander'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { promises as fs, createReadStream } from 'fs'
|
|
4
|
-
import { findGlotstackConfig } from './util/findConfig'
|
|
4
|
+
import { findGlotstackConfig, GlotstackConfig } from './util/findConfig'
|
|
5
5
|
import { cwd } from 'process'
|
|
6
6
|
import { merge } from './util/object'
|
|
7
7
|
import { loadYaml } from './util/yaml'
|
|
@@ -67,27 +67,34 @@ function unflatten(flat: Record<string, any>): Record<string, any> {
|
|
|
67
67
|
const DEFAULT_OPTIONS: Record<string, string> = {
|
|
68
68
|
sourcePath: '.',
|
|
69
69
|
sourceLocale: 'en-US',
|
|
70
|
-
apiOrigin: 'https://glotstack.ai'
|
|
70
|
+
apiOrigin: 'https://glotstack.ai',
|
|
71
|
+
yaml: 'false',
|
|
71
72
|
}
|
|
72
73
|
|
|
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
|
+
}
|
|
74
80
|
|
|
75
|
-
const config = await findGlotstackConfig(cwd()) ?? {}
|
|
76
81
|
|
|
77
|
-
|
|
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
|
-
}
|
|
82
|
+
async function resolveConfigAndOptions(options: Record<string, any>): Promise<GlotstackConfig & { yes?: boolean; yaml?: boolean }> {
|
|
83
83
|
|
|
84
|
-
const
|
|
84
|
+
const config = await findGlotstackConfig(cwd()) ?? {}
|
|
85
|
+
const resolved = merge<GlotstackConfig>({} as GlotstackConfig, DEFAULT_OPTIONS, config, options)
|
|
85
86
|
|
|
86
87
|
// special case to match source
|
|
87
88
|
if (resolved.outputDir == null) {
|
|
88
89
|
resolved.outputDir = resolved.sourcePath
|
|
89
90
|
}
|
|
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
|
+
}
|
|
91
98
|
return resolved
|
|
92
99
|
}
|
|
93
100
|
|
|
@@ -98,18 +105,20 @@ async function run(args: string[]) {
|
|
|
98
105
|
.description('extract translations from all compatible source files.')
|
|
99
106
|
.option('--source-path [path]', `path directory containing [locale].json files (default=${DEFAULT_OPTIONS['sourcePath']})`)
|
|
100
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')
|
|
101
109
|
.option('--api-origin [url]', `glotstack api origin (default=${DEFAULT_OPTIONS['apiOrigin']})`)
|
|
102
110
|
.option('--output-dir [path]', 'path to output directory (default=<source-path>')
|
|
103
111
|
.option('--api-key [key]', 'api key for glotstack.ai')
|
|
104
112
|
.option('--yes', 'skip confirm checks')
|
|
105
|
-
.
|
|
113
|
+
.argument('[directories...]', 'Directories to scan', './**/*')
|
|
114
|
+
.action(async (directories: string[], inputOptions: Record<string, any>) => {
|
|
106
115
|
const options = await resolveConfigAndOptions(inputOptions)
|
|
107
116
|
if (!options.apiOrigin) {
|
|
108
117
|
throw new Error('apiOrigin must be specified')
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
const linter = new eslint.ESLint({ overrideConfigFile: path.join(__dirname, '..', 'eslint-raw-string.mjs') })
|
|
112
|
-
const results = await linter.lintFiles(
|
|
121
|
+
const results = await linter.lintFiles(directories)
|
|
113
122
|
const filesWithIssues = results
|
|
114
123
|
.filter((r) => r.errorCount + r.warningCount > 0)
|
|
115
124
|
.map((r) => r.filePath)
|
|
@@ -133,7 +142,12 @@ async function run(args: string[]) {
|
|
|
133
142
|
const send = await askToSend()
|
|
134
143
|
if (send) {
|
|
135
144
|
console.info('Sending files to generate new source and extracted strings')
|
|
136
|
-
|
|
145
|
+
let url = `${options.apiOrigin}/uploads/translations/extract`
|
|
146
|
+
|
|
147
|
+
if (options.yaml) {
|
|
148
|
+
url = `${url}?yaml=true`
|
|
149
|
+
}
|
|
150
|
+
|
|
137
151
|
const form = new FormData()
|
|
138
152
|
|
|
139
153
|
for (let i = 0; i < filesWithIssues.length; i++) {
|
|
@@ -154,10 +168,12 @@ async function run(args: string[]) {
|
|
|
154
168
|
.description('fetch translations for all [output-locals...]. Use .glotstack.json for repeatable results.')
|
|
155
169
|
.option('--source-path [path]', `path directory containing [locale].json files (default=${DEFAULT_OPTIONS['sourcePath']})`)
|
|
156
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')
|
|
157
172
|
.option('--api-origin [url]', `glotstack api origin (default=${DEFAULT_OPTIONS['apiOrigin']})`)
|
|
158
173
|
.option('--output-dir [path]', 'path to output directory (default=<source-path>')
|
|
159
174
|
.option('--api-key [key]', 'api key for glotstack.ai')
|
|
160
175
|
.option('--project-id [id]', '(optional) specific project to use')
|
|
176
|
+
.option('--only [locale]', '(optional) only translate for this locale')
|
|
161
177
|
.argument('[output-locales...]', 'locales to get translations for')
|
|
162
178
|
.action(async (outputLocales: string[], options: Record<string, any>, command: Command) => {
|
|
163
179
|
const resolved = await resolveConfigAndOptions({ ...options, outputLocales: outputLocales })
|
|
@@ -171,7 +187,8 @@ async function run(args: string[]) {
|
|
|
171
187
|
throw new Error('outputDir must be specified')
|
|
172
188
|
}
|
|
173
189
|
|
|
174
|
-
const
|
|
190
|
+
const ext = options.yaml == true ? '.yaml' : '.json'
|
|
191
|
+
const absPath = path.resolve(resolved.sourcePath, `${resolved.sourceLocale}${ext}`)
|
|
175
192
|
const fileContent = await fs.readFile(absPath, 'utf-8')
|
|
176
193
|
|
|
177
194
|
let json = null
|
|
@@ -185,23 +202,50 @@ async function run(args: string[]) {
|
|
|
185
202
|
throw err
|
|
186
203
|
}
|
|
187
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
|
+
}
|
|
188
209
|
|
|
189
210
|
const body = {
|
|
190
|
-
locales
|
|
211
|
+
locales,
|
|
191
212
|
translations: json,
|
|
213
|
+
usage: options.usage,
|
|
192
214
|
...{ ... (resolved.projectId != null ? { projectId: resolved.projectId } : {}) },
|
|
193
215
|
}
|
|
194
216
|
|
|
195
217
|
const url = `${resolved.apiOrigin}/api/translations`
|
|
218
|
+
console.info('Getting translations for: ', locales)
|
|
196
219
|
const data = await fetchGlotstack<{ data: Translations }>(url, resolved.apiKey, body)
|
|
197
220
|
console.info('Received translations:', data)
|
|
198
221
|
Object.entries(data.data).map(([key, val]) => {
|
|
199
222
|
const p = `${resolved.outputDir}/${key}.json`
|
|
200
223
|
console.info(`Writing file ${p}`)
|
|
201
|
-
fs.writeFile(`${resolved.outputDir}/${key}.json`, JSON.stringify(val, null, 2))
|
|
224
|
+
fs.writeFile(`${resolved.outputDir}/${key}.json`, JSON.stringify(val, null, 2), 'utf-8')
|
|
202
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')
|
|
203
246
|
})
|
|
204
247
|
|
|
248
|
+
|
|
205
249
|
program
|
|
206
250
|
.command('format-json')
|
|
207
251
|
.description('format files in --source-path [path] to nested (not flat)')
|
|
@@ -236,7 +280,7 @@ async function run(args: string[]) {
|
|
|
236
280
|
const text = await readFile(fp, 'utf-8')
|
|
237
281
|
const json = JSON.parse(text)
|
|
238
282
|
const formatted = JSON.stringify(unflatten(json), null, 2)
|
|
239
|
-
await writeFile(fp, formatted)
|
|
283
|
+
await writeFile(fp, formatted, 'utf-8')
|
|
240
284
|
}
|
|
241
285
|
rl.close()
|
|
242
286
|
}
|
package/src/index.tsx
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as React from 'react'
|
|
2
2
|
import { merge } from './util/object'
|
|
3
|
-
import { debug } from 'node:console'
|
|
4
3
|
|
|
5
4
|
export type LocaleRegion = string
|
|
6
5
|
|
|
@@ -14,15 +13,27 @@ export interface Translations {
|
|
|
14
13
|
[key: string]: Translations | TranslationLeaf
|
|
15
14
|
}
|
|
16
15
|
|
|
16
|
+
interface TranslateOptsBase {
|
|
17
|
+
locale?: LocaleRegion
|
|
18
|
+
assigns?: Record<string, React.ReactNode>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TranslateFn = {
|
|
22
|
+
(key: string, opts: TranslateOptsBase & { asString: true }): string
|
|
23
|
+
(
|
|
24
|
+
key: string,
|
|
25
|
+
opts?: TranslateOptsBase & { asString?: false },
|
|
26
|
+
): React.ReactNode
|
|
27
|
+
}
|
|
28
|
+
|
|
17
29
|
|
|
18
30
|
export interface ContextType {
|
|
19
|
-
translations: Translations
|
|
31
|
+
translations: Record<string, Translations>
|
|
20
32
|
locale: string | null
|
|
21
33
|
loadTranslations: (locale: LocaleRegion) => Promise<Translations>
|
|
22
34
|
setLocale: (locale: LocaleRegion) => void
|
|
23
|
-
importMethod: (locale: LocaleRegion) => Promise<Translations>
|
|
24
|
-
t:
|
|
25
|
-
ssr: boolean
|
|
35
|
+
importMethod: (locale: LocaleRegion) => Promise<Translations>
|
|
36
|
+
t: TranslateFn
|
|
26
37
|
}
|
|
27
38
|
|
|
28
39
|
export const GlotstackContext = React.createContext<ContextType>({
|
|
@@ -32,7 +43,6 @@ export const GlotstackContext = React.createContext<ContextType>({
|
|
|
32
43
|
locale: null,
|
|
33
44
|
importMethod: (_locale: LocaleRegion) => { throw new Error('import method not set') },
|
|
34
45
|
t: () => { throw new Error('import method not set') },
|
|
35
|
-
ssr: false
|
|
36
46
|
})
|
|
37
47
|
|
|
38
48
|
interface GlotstackProviderProps {
|
|
@@ -53,6 +63,14 @@ export enum LogLevel {
|
|
|
53
63
|
ERROR = 4,
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
const LogName = {
|
|
67
|
+
[LogLevel.DEBUG]: 'debug',
|
|
68
|
+
[LogLevel.LOG]: 'log',
|
|
69
|
+
[LogLevel.INFO]: 'info',
|
|
70
|
+
[LogLevel.WARNING]: 'warning',
|
|
71
|
+
[LogLevel.ERROR]: 'error',
|
|
72
|
+
} as const
|
|
73
|
+
|
|
56
74
|
const LogLevelToFunc: Record<LogLevel, (...args: Parameters<typeof console.info>) => void> = {
|
|
57
75
|
[LogLevel.DEBUG]: console.debug,
|
|
58
76
|
[LogLevel.INFO]: console.info,
|
|
@@ -72,7 +90,7 @@ const makeLoggingFunction = (level: LogLevel) => (...args: Parameters<typeof con
|
|
|
72
90
|
if (level < logLevel) {
|
|
73
91
|
return
|
|
74
92
|
}
|
|
75
|
-
return func(`[
|
|
93
|
+
return func(`[${LogName[level]}][glotstack.ai]`, ...args)
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
const logger = {
|
|
@@ -102,15 +120,221 @@ export const access = (key: string, locale: LocaleRegion, translations: Translat
|
|
|
102
120
|
return (value?.value ?? key) as string
|
|
103
121
|
}
|
|
104
122
|
|
|
105
|
-
|
|
123
|
+
|
|
124
|
+
function isAsStringTrue(
|
|
125
|
+
opts: (TranslateOptsBase & { asString?: boolean }) | undefined,
|
|
126
|
+
): opts is TranslateOptsBase & { asString: true } {
|
|
127
|
+
return opts?.asString === true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function translate(
|
|
131
|
+
key: string,
|
|
132
|
+
opts: {
|
|
133
|
+
locale?: LocaleRegion
|
|
134
|
+
assigns?: Record<string, React.ReactNode>
|
|
135
|
+
asString?: boolean
|
|
136
|
+
glotstack: ReturnType<typeof useGlotstack>
|
|
137
|
+
globalLocale: LocaleRegion
|
|
138
|
+
translations: Record<LocaleRegion, Translations>
|
|
139
|
+
localeRef: React.MutableRefObject<LocaleRegion>
|
|
140
|
+
accessedRef: React.MutableRefObject<Record<string, Record<string, string>>>
|
|
141
|
+
extractionsRef: React.MutableRefObject<
|
|
142
|
+
Record<
|
|
143
|
+
string,
|
|
144
|
+
Record<string, ParsedSimplePlaceholder[] | undefined> | undefined
|
|
145
|
+
>
|
|
146
|
+
>
|
|
147
|
+
outputRef: React.MutableRefObject<
|
|
148
|
+
Record<string, Record<string, React.ReactNode | undefined> | undefined>
|
|
149
|
+
>
|
|
150
|
+
},
|
|
151
|
+
): string | React.ReactNode {
|
|
152
|
+
const {
|
|
153
|
+
glotstack,
|
|
154
|
+
globalLocale,
|
|
155
|
+
translations,
|
|
156
|
+
localeRef,
|
|
157
|
+
accessedRef,
|
|
158
|
+
extractionsRef,
|
|
159
|
+
outputRef,
|
|
160
|
+
} = opts
|
|
161
|
+
const locale = opts?.locale ?? globalLocale
|
|
162
|
+
localeRef.current = locale
|
|
163
|
+
glotstack.loadTranslations(localeRef.current)
|
|
164
|
+
|
|
165
|
+
let string = ''
|
|
166
|
+
if (translations != null) {
|
|
167
|
+
string = access(key, locale, translations ?? {})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (string === key) return key
|
|
171
|
+
|
|
172
|
+
if (outputRef.current == null) {
|
|
173
|
+
outputRef.current = {}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!outputRef.current[locale]) {
|
|
177
|
+
outputRef.current[locale] = {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!accessedRef.current[locale]) {
|
|
181
|
+
accessedRef.current[locale] = {}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
outputRef.current[locale]?.[key] != null &&
|
|
186
|
+
string === accessedRef.current[locale]?.[key]
|
|
187
|
+
) {
|
|
188
|
+
return outputRef.current[locale]![key]
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
accessedRef.current[locale] ??= {}
|
|
192
|
+
accessedRef.current[locale][key] = string
|
|
193
|
+
|
|
194
|
+
extractionsRef.current[locale] ??= {}
|
|
195
|
+
if (!extractionsRef.current[locale]![key]) {
|
|
196
|
+
const newExtractions = extractSimplePlaceholders(string)
|
|
197
|
+
extractionsRef.current[locale]![key]! = newExtractions
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (outputRef.current[locale] == null) {
|
|
201
|
+
outputRef.current[locale] = {}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (outputRef.current[locale]![key] == null) {
|
|
205
|
+
const output = renderPlaceholdersToNodes(
|
|
206
|
+
string,
|
|
207
|
+
extractionsRef.current[locale]![key]!,
|
|
208
|
+
opts?.assigns ?? {},
|
|
209
|
+
)
|
|
210
|
+
outputRef.current[locale]![key] = output
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let output = outputRef.current[locale]![key]
|
|
214
|
+
|
|
215
|
+
if (isAsStringTrue(opts)) {
|
|
216
|
+
// Convert any possible value to string safely
|
|
217
|
+
output = Array.isArray(output)
|
|
218
|
+
? output.join('')
|
|
219
|
+
: typeof output === 'string'
|
|
220
|
+
? output
|
|
221
|
+
: String(output ?? '')
|
|
222
|
+
|
|
223
|
+
if (outputRef.current != null) {
|
|
224
|
+
outputRef.current[locale]![key] = output
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return output as React.ReactNode
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function createTranslate(
|
|
231
|
+
glotstack: ReturnType<typeof useGlotstack>,
|
|
232
|
+
globalLocale: LocaleRegion,
|
|
233
|
+
translations: Record<LocaleRegion, Translations>,
|
|
234
|
+
localeRef: React.MutableRefObject<LocaleRegion>,
|
|
235
|
+
accessedRef: React.MutableRefObject<Record<string, Record<string, string>>>,
|
|
236
|
+
extractionsRef: React.MutableRefObject<
|
|
237
|
+
Record<
|
|
238
|
+
string,
|
|
239
|
+
Record<string, ParsedSimplePlaceholder[] | undefined> | undefined
|
|
240
|
+
>
|
|
241
|
+
>,
|
|
242
|
+
outputRef: React.MutableRefObject<
|
|
243
|
+
Record<string, Record<string, React.ReactNode | undefined> | undefined>
|
|
244
|
+
>,
|
|
245
|
+
): TranslateFn {
|
|
246
|
+
function t(
|
|
247
|
+
key: string,
|
|
248
|
+
opts?: {
|
|
249
|
+
locale?: LocaleRegion
|
|
250
|
+
assigns?: Record<string, React.ReactNode>
|
|
251
|
+
asString: true
|
|
252
|
+
},
|
|
253
|
+
): string
|
|
254
|
+
function t(
|
|
255
|
+
key: string,
|
|
256
|
+
opts?: {
|
|
257
|
+
locale?: LocaleRegion
|
|
258
|
+
assigns?: Record<string, React.ReactNode>
|
|
259
|
+
asString?: false
|
|
260
|
+
},
|
|
261
|
+
): React.ReactNode
|
|
262
|
+
function t(
|
|
263
|
+
key: string,
|
|
264
|
+
opts?: {
|
|
265
|
+
locale?: LocaleRegion
|
|
266
|
+
assigns?: Record<string, React.ReactNode>
|
|
267
|
+
asString?: boolean
|
|
268
|
+
},
|
|
269
|
+
) {
|
|
270
|
+
return translate(
|
|
271
|
+
key,
|
|
272
|
+
merge<Parameters<typeof translate>[1]>(
|
|
273
|
+
{
|
|
274
|
+
glotstack,
|
|
275
|
+
globalLocale,
|
|
276
|
+
translations,
|
|
277
|
+
localeRef,
|
|
278
|
+
accessedRef,
|
|
279
|
+
extractionsRef,
|
|
280
|
+
outputRef,
|
|
281
|
+
},
|
|
282
|
+
opts ?? {},
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return t
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// export const useReduxGlotstack = () => {
|
|
291
|
+
// const glotstack = useGlotstack()
|
|
292
|
+
// const globalLocale = useSelector(selectLocale)
|
|
293
|
+
// const localeRef = React.useRef(globalLocale)
|
|
294
|
+
// localeRef.current = globalLocale
|
|
295
|
+
|
|
296
|
+
// const translations = useSelector(selectTranslations)
|
|
297
|
+
|
|
298
|
+
// React.useEffect(() => {
|
|
299
|
+
// glotstack.loadTranslations(localeRef.current)
|
|
300
|
+
// }, [localeRef.current])
|
|
301
|
+
|
|
302
|
+
// const accessedRef = React.useRef<Record<string, Record<string, string>>>({})
|
|
303
|
+
// const extractionsRef = React.useRef<
|
|
304
|
+
// Record<string, Record<string, ParsedSimplePlaceholder[]>>
|
|
305
|
+
// >({})
|
|
306
|
+
// const outputRef = React.useRef<
|
|
307
|
+
// Record<string, Record<string, React.ReactNode>>
|
|
308
|
+
// >({})
|
|
309
|
+
|
|
310
|
+
// const t: TranslateFn = React.useMemo(() => {
|
|
311
|
+
// return createTranslate(
|
|
312
|
+
// glotstack,
|
|
313
|
+
// globalLocale,
|
|
314
|
+
// translations ?? {},
|
|
315
|
+
// localeRef,
|
|
316
|
+
// accessedRef,
|
|
317
|
+
// extractionsRef,
|
|
318
|
+
// outputRef,
|
|
319
|
+
// )
|
|
320
|
+
// }, [globalLocale, translations])
|
|
321
|
+
|
|
322
|
+
// return { t }
|
|
323
|
+
// }
|
|
324
|
+
|
|
325
|
+
export const GlotstackProvider = ({ children, initialLocale, initialTranslations, onLocaleChange, onTranslationLoaded, importMethod }: GlotstackProviderProps) => {
|
|
106
326
|
if (initialLocale == null) {
|
|
107
327
|
throw new Error('initialLocale must be set')
|
|
108
328
|
}
|
|
109
329
|
const [locale, setLocale] = React.useState<LocaleRegion>(initialLocale)
|
|
110
|
-
const translationsRef = React.useRef<Record<string, Translations
|
|
330
|
+
const translationsRef = React.useRef<Record<string, Translations> | null>(initialTranslations || null)
|
|
331
|
+
const accessedRef = React.useRef<Record<string, Record<string, string>>>({})
|
|
332
|
+
const outputRef = React.useRef<Record<string, Record<string, React.ReactNode>>>({})
|
|
333
|
+
const extractionsRef = React.useRef<Record<string, Record<string, ParsedSimplePlaceholder[]>>>({})
|
|
111
334
|
const loadingRef = React.useRef<Record<string, Promise<Translations>>>({})
|
|
335
|
+
const localeRef = React.useRef<string>('en-US')
|
|
112
336
|
|
|
113
|
-
const loadTranslations = React.useCallback(async (locale: string, opts?: {force?: boolean}) => {
|
|
337
|
+
const loadTranslations = React.useCallback(async (locale: string, opts?: { force?: boolean }) => {
|
|
114
338
|
// TODO: if translations are loaded only reload if some condition is
|
|
115
339
|
try {
|
|
116
340
|
if (loadingRef.current?.[locale] != null && opts?.force != true) {
|
|
@@ -152,20 +376,30 @@ export const GlotstackProvider = ({ children, initialLocale, initialTranslations
|
|
|
152
376
|
}, [locale])
|
|
153
377
|
|
|
154
378
|
const context = React.useMemo(() => {
|
|
155
|
-
|
|
379
|
+
const context: ContextType = {
|
|
156
380
|
setLocale,
|
|
157
381
|
translations: translationsRef.current ?? {},
|
|
158
382
|
locale,
|
|
159
383
|
importMethod,
|
|
160
384
|
loadTranslations,
|
|
161
|
-
t: (
|
|
162
|
-
const resolvedLocale = opts?.locale ?? locale
|
|
163
|
-
loadTranslations(resolvedLocale)
|
|
164
|
-
return access(key, opts?.locale ?? locale, translationsRef.current ?? {})
|
|
165
|
-
},
|
|
166
|
-
ssr: ssr == true,
|
|
385
|
+
t: () => '',
|
|
167
386
|
}
|
|
168
|
-
|
|
387
|
+
localeRef.current = locale
|
|
388
|
+
|
|
389
|
+
const t = createTranslate(
|
|
390
|
+
context,
|
|
391
|
+
context.locale ?? 'en-US',
|
|
392
|
+
context.translations ?? {},
|
|
393
|
+
localeRef,
|
|
394
|
+
accessedRef,
|
|
395
|
+
extractionsRef,
|
|
396
|
+
outputRef,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
context.t = t
|
|
400
|
+
return context
|
|
401
|
+
|
|
402
|
+
}, [locale, importMethod, loadTranslations])
|
|
169
403
|
|
|
170
404
|
return <GlotstackContext.Provider value={context}>
|
|
171
405
|
{children}
|
|
@@ -181,3 +415,104 @@ export const useTranslations = (_options?: Record<never, never>) => {
|
|
|
181
415
|
return context
|
|
182
416
|
}
|
|
183
417
|
|
|
418
|
+
|
|
419
|
+
export type ParsedSimplePlaceholder = {
|
|
420
|
+
key: string
|
|
421
|
+
options: string[]
|
|
422
|
+
raw: string
|
|
423
|
+
index: number
|
|
424
|
+
kind: 'doubleCurly' | 'component'
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const curlyRegex = /(?<!\\)({{\s*([a-zA-Z0-9_]+)\s*(?:,\s*([^{}]*?))?\s*}})/g
|
|
428
|
+
const componentRegex = /<([A-Z][a-zA-Z0-9]*)>([\s\S]*?)<\/\1>/g
|
|
429
|
+
|
|
430
|
+
export function extractSimplePlaceholders(input: string): ParsedSimplePlaceholder[] {
|
|
431
|
+
const results: ParsedSimplePlaceholder[] = []
|
|
432
|
+
|
|
433
|
+
for (const match of input.matchAll(curlyRegex)) {
|
|
434
|
+
const raw = match[1]
|
|
435
|
+
const key = match[2]
|
|
436
|
+
const rawOptions = match[3]
|
|
437
|
+
const index = match.index ?? -1
|
|
438
|
+
|
|
439
|
+
const options = rawOptions
|
|
440
|
+
? rawOptions.split(',').map(opt => opt.trim()).filter(Boolean)
|
|
441
|
+
: []
|
|
442
|
+
|
|
443
|
+
results.push({ key, options, raw, index, kind: 'doubleCurly' })
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (const match of input.matchAll(componentRegex)) {
|
|
447
|
+
const raw = match[0]
|
|
448
|
+
const key = match[1]
|
|
449
|
+
const index = match.index ?? -1
|
|
450
|
+
|
|
451
|
+
results.push({ key, options: [], raw, index, kind: 'component' })
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return results.sort((a, b) => a.index - b.index)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
type Renderer = (props: { children: React.ReactNode }) => React.ReactNode
|
|
458
|
+
|
|
459
|
+
export function renderPlaceholdersToNodes(
|
|
460
|
+
input: string,
|
|
461
|
+
placeholders: ParsedSimplePlaceholder[],
|
|
462
|
+
assigns: Record<string, React.ReactNode | Renderer>
|
|
463
|
+
): React.ReactNode[] {
|
|
464
|
+
const nodes: React.ReactNode[] = []
|
|
465
|
+
let cursor = 0
|
|
466
|
+
|
|
467
|
+
for (const { index, raw, key, kind } of placeholders) {
|
|
468
|
+
if (cursor < index) {
|
|
469
|
+
nodes.push(input.slice(cursor, index))
|
|
470
|
+
}
|
|
471
|
+
let value: React.ReactNode = raw
|
|
472
|
+
|
|
473
|
+
if (kind === 'component') {
|
|
474
|
+
const Render = assigns[key]
|
|
475
|
+
const inner = raw.replace(new RegExp(`^<${key}>`), '').replace(new RegExp(`</${key}>$`), '')
|
|
476
|
+
|
|
477
|
+
if (React.isValidElement(Render)) {
|
|
478
|
+
value = React.cloneElement(Render, {}, inner)
|
|
479
|
+
} else if (typeof Render === 'function') {
|
|
480
|
+
value = <Render>{inner}</Render>
|
|
481
|
+
} else {
|
|
482
|
+
logger.warn(`Invalid assign substitution for:\n\n ${raw}\n\nDid you remember to pass assigns?\n`,
|
|
483
|
+
`
|
|
484
|
+
t('key', { assigns: {
|
|
485
|
+
${key}: <something /> // children will be copied via React.cloneElement
|
|
486
|
+
}})\n\nor\n
|
|
487
|
+
t('key', { assigns: {
|
|
488
|
+
${key}: MyComponent // component will be rendered with <Component/>
|
|
489
|
+
}})\n
|
|
490
|
+
`
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
} else if (kind === 'doubleCurly') {
|
|
494
|
+
const Render = assigns[key]
|
|
495
|
+
value = typeof Render !== 'function' ? Render : raw ?? raw
|
|
496
|
+
}
|
|
497
|
+
nodes.push(value)
|
|
498
|
+
cursor = index + raw.length
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (cursor < input.length) {
|
|
502
|
+
nodes.push(input.slice(cursor))
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Unescape \{{...}} to {{...}}, and wrap ReactNodes
|
|
506
|
+
return nodes.map((node, i) =>
|
|
507
|
+
typeof node === 'string'
|
|
508
|
+
? node.replace(/\\({{[^{}]+}})/g, '$1')
|
|
509
|
+
: <React.Fragment key={i}>{node}</React.Fragment>
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
export function useRenderPlaceholdersToNodes(...args: Parameters<typeof renderPlaceholdersToNodes>) {
|
|
516
|
+
const nodes = React.useMemo(() => renderPlaceholdersToNodes(...args), [...args])
|
|
517
|
+
return nodes
|
|
518
|
+
}
|