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/src/index.tsx
DELETED
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
import * as React from 'react'
|
|
2
|
-
import { merge } from './util/object'
|
|
3
|
-
|
|
4
|
-
export type LocaleRegion = string
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export interface TranslationLeaf {
|
|
8
|
-
value: string
|
|
9
|
-
context?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface Translations {
|
|
13
|
-
[key: string]: Translations | TranslationLeaf
|
|
14
|
-
}
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
export interface ContextType {
|
|
31
|
-
translations: Record<string, Translations>
|
|
32
|
-
locale: string | null
|
|
33
|
-
loadTranslations: (locale: LocaleRegion) => Promise<Translations>
|
|
34
|
-
setLocale: (locale: LocaleRegion) => void
|
|
35
|
-
importMethod: (locale: LocaleRegion) => Promise<Translations>
|
|
36
|
-
t: TranslateFn
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const GlotstackContext = React.createContext<ContextType>({
|
|
40
|
-
translations: {},
|
|
41
|
-
loadTranslations: () => { throw new Error('no import method set') },
|
|
42
|
-
setLocale: (_locale: LocaleRegion) => { throw new Error('import method not set') },
|
|
43
|
-
locale: null,
|
|
44
|
-
importMethod: (_locale: LocaleRegion) => { throw new Error('import method not set') },
|
|
45
|
-
t: () => { throw new Error('import method not set') },
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
interface GlotstackProviderProps {
|
|
49
|
-
children: React.ReactNode
|
|
50
|
-
initialTranslations?: Record<string, Translations>
|
|
51
|
-
initialLocale?: LocaleRegion
|
|
52
|
-
onTranslationLoaded?: (locale: LocaleRegion, translations: Translations) => void
|
|
53
|
-
onLocaleChange?: (locale: LocaleRegion) => void
|
|
54
|
-
importMethod: ContextType['importMethod']
|
|
55
|
-
ssr?: boolean
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export enum LogLevel {
|
|
59
|
-
DEBUG = 0,
|
|
60
|
-
LOG = 1,
|
|
61
|
-
INFO = 2,
|
|
62
|
-
WARNING = 3,
|
|
63
|
-
ERROR = 4,
|
|
64
|
-
}
|
|
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
|
-
|
|
74
|
-
const LogLevelToFunc: Record<LogLevel, (...args: Parameters<typeof console.info>) => void> = {
|
|
75
|
-
[LogLevel.DEBUG]: console.debug,
|
|
76
|
-
[LogLevel.INFO]: console.info,
|
|
77
|
-
[LogLevel.LOG]: console.log,
|
|
78
|
-
[LogLevel.WARNING]: console.warn,
|
|
79
|
-
[LogLevel.ERROR]: console.error,
|
|
80
|
-
} as const
|
|
81
|
-
|
|
82
|
-
let logLevel: LogLevel = LogLevel.DEBUG
|
|
83
|
-
|
|
84
|
-
export const setLogLevel = (level: LogLevel) => {
|
|
85
|
-
logLevel = level
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const makeLoggingFunction = (level: LogLevel) => (...args: Parameters<typeof console.info>) => {
|
|
89
|
-
const func = LogLevelToFunc[level]
|
|
90
|
-
if (level < logLevel) {
|
|
91
|
-
return
|
|
92
|
-
}
|
|
93
|
-
return func(`[${LogName[level]}][glotstack.ai]`, ...args)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const logger = {
|
|
97
|
-
debug: makeLoggingFunction(LogLevel.DEBUG),
|
|
98
|
-
info: makeLoggingFunction(LogLevel.INFO),
|
|
99
|
-
warn: makeLoggingFunction(LogLevel.WARNING),
|
|
100
|
-
error: makeLoggingFunction(LogLevel.ERROR),
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const access = (key: string, locale: LocaleRegion, translations: Translations) => {
|
|
105
|
-
if (translations == null) {
|
|
106
|
-
return key
|
|
107
|
-
}
|
|
108
|
-
const access = [...key.split('.')] as [LocaleRegion, ...string[]]
|
|
109
|
-
const localeTranslations = translations?.[locale]
|
|
110
|
-
|
|
111
|
-
if (localeTranslations == null) {
|
|
112
|
-
return key
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const value = access.reduce((acc: Translations[string], key) => {
|
|
116
|
-
// @ts-expect-error expected
|
|
117
|
-
return acc?.[key]
|
|
118
|
-
}, localeTranslations)
|
|
119
|
-
|
|
120
|
-
return (value?.value ?? key) as string
|
|
121
|
-
}
|
|
122
|
-
|
|
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) => {
|
|
326
|
-
if (initialLocale == null) {
|
|
327
|
-
throw new Error('initialLocale must be set')
|
|
328
|
-
}
|
|
329
|
-
const [locale, setLocale] = React.useState<LocaleRegion>(initialLocale)
|
|
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[]>>>({})
|
|
334
|
-
const loadingRef = React.useRef<Record<string, Promise<Translations>>>({})
|
|
335
|
-
const localeRef = React.useRef<string>('en-US')
|
|
336
|
-
|
|
337
|
-
const loadTranslations = React.useCallback(async (locale: string, opts?: { force?: boolean }) => {
|
|
338
|
-
// TODO: if translations are loaded only reload if some condition is
|
|
339
|
-
try {
|
|
340
|
-
if (loadingRef.current?.[locale] != null && opts?.force != true) {
|
|
341
|
-
logger.debug('Waiting for translations already loading', locale)
|
|
342
|
-
return (await loadingRef.current?.[locale])
|
|
343
|
-
}
|
|
344
|
-
if (translationsRef.current?.[locale] != null && opts?.force != true) {
|
|
345
|
-
logger.debug('Skipping load for translations', locale, translationsRef.current?.[locale], translationsRef.current)
|
|
346
|
-
return translationsRef.current?.[locale]
|
|
347
|
-
}
|
|
348
|
-
if (loadingRef.current != null) {
|
|
349
|
-
logger.debug('Loading translations', locale, merge({}, translationsRef.current ?? {}))
|
|
350
|
-
loadingRef.current[locale] = importMethod(locale)
|
|
351
|
-
}
|
|
352
|
-
const result = await loadingRef.current[locale]
|
|
353
|
-
|
|
354
|
-
if (result == null) {
|
|
355
|
-
throw new Error(`Failed to load translation ${locale} ${JSON.stringify(result)}`)
|
|
356
|
-
}
|
|
357
|
-
if (translationsRef.current) {
|
|
358
|
-
translationsRef.current[locale] = result
|
|
359
|
-
}
|
|
360
|
-
onTranslationLoaded?.(locale, result)
|
|
361
|
-
return result
|
|
362
|
-
} catch (err) {
|
|
363
|
-
logger.error('Unable to import translations', err)
|
|
364
|
-
throw err
|
|
365
|
-
}
|
|
366
|
-
}, [importMethod, onTranslationLoaded])
|
|
367
|
-
|
|
368
|
-
React.useEffect(() => {
|
|
369
|
-
const run = async () => {
|
|
370
|
-
onLocaleChange?.(locale)
|
|
371
|
-
await loadTranslations(locale)
|
|
372
|
-
}
|
|
373
|
-
React.startTransition(() => {
|
|
374
|
-
run()
|
|
375
|
-
})
|
|
376
|
-
}, [locale])
|
|
377
|
-
|
|
378
|
-
const context = React.useMemo(() => {
|
|
379
|
-
const context: ContextType = {
|
|
380
|
-
setLocale,
|
|
381
|
-
translations: translationsRef.current ?? {},
|
|
382
|
-
locale,
|
|
383
|
-
importMethod,
|
|
384
|
-
loadTranslations,
|
|
385
|
-
t: () => '',
|
|
386
|
-
}
|
|
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])
|
|
403
|
-
|
|
404
|
-
return <GlotstackContext.Provider value={context}>
|
|
405
|
-
{children}
|
|
406
|
-
</GlotstackContext.Provider>
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
export const useGlotstack = () => {
|
|
410
|
-
return React.useContext(GlotstackContext)
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export const useTranslations = (_options?: Record<never, never>) => {
|
|
414
|
-
const context = React.useContext(GlotstackContext)
|
|
415
|
-
return context
|
|
416
|
-
}
|
|
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
|
-
}
|
package/src/util/findConfig.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'fs'
|
|
2
|
-
import { readFile } from 'fs/promises'
|
|
3
|
-
import * as path from 'path'
|
|
4
|
-
import { loadYaml } from './yaml'
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function dropKeys<T>(input: T | string, keysToDrop: string[]): T {
|
|
8
|
-
const obj = typeof input === 'string' ? JSON.parse(input) : structuredClone(input)
|
|
9
|
-
|
|
10
|
-
for (const path of keysToDrop) {
|
|
11
|
-
const segments = path.split('.')
|
|
12
|
-
let target: any = obj
|
|
13
|
-
|
|
14
|
-
for (let i = 0; i < segments.length - 1; i++) {
|
|
15
|
-
const key = segments[i]
|
|
16
|
-
const next = isNaN(Number(key)) ? key : Number(key)
|
|
17
|
-
|
|
18
|
-
if (target[next] === undefined) {
|
|
19
|
-
target = undefined
|
|
20
|
-
break
|
|
21
|
-
}
|
|
22
|
-
target = target[next]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (target !== undefined) {
|
|
26
|
-
const finalKey = segments[segments.length - 1]
|
|
27
|
-
const final = isNaN(Number(finalKey)) ? finalKey : Number(finalKey)
|
|
28
|
-
delete target[final]
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return obj
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export interface GlotstackConfig {
|
|
37
|
-
outputDir?: string
|
|
38
|
-
sourcePath: string
|
|
39
|
-
sourceLocale: string
|
|
40
|
-
outputLocales: string[]
|
|
41
|
-
apiOrigin?: string
|
|
42
|
-
apiKey: string
|
|
43
|
-
projectId?: string
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Recursively looks for `.glotstack.json` from the current directory up to the root.
|
|
48
|
-
* @param startDir The directory to start the search from. Defaults to process.cwd().
|
|
49
|
-
* @returns The absolute path to the file if found, or null if not found.
|
|
50
|
-
*/
|
|
51
|
-
export async function findGlotstackConfig(startDir: string = process.cwd()): Promise<GlotstackConfig | null> {
|
|
52
|
-
let currentDir = path.resolve(startDir)
|
|
53
|
-
let configPath = null
|
|
54
|
-
|
|
55
|
-
while (true) {
|
|
56
|
-
const jsonCandidate = path.join(currentDir, '.glotstack.json')
|
|
57
|
-
const yamlCandidate = path.join(currentDir, '.glotstack.yaml')
|
|
58
|
-
const jsonExists = existsSync(jsonCandidate)
|
|
59
|
-
const yamlExists = existsSync(yamlCandidate)
|
|
60
|
-
if (jsonExists && yamlExists) {
|
|
61
|
-
console.error('Both .glotstack.json and .glotstack.yaml exist, please delete one\n\n json: ', jsonCandidate, '\n yaml: ', yamlCandidate)
|
|
62
|
-
throw new Error('Two config formats cannot be used at the same time')
|
|
63
|
-
|
|
64
|
-
} else if (jsonExists) {
|
|
65
|
-
configPath = jsonCandidate
|
|
66
|
-
} else if (yamlExists) {
|
|
67
|
-
configPath = yamlCandidate
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const parentDir = path.dirname(currentDir)
|
|
71
|
-
if (parentDir === currentDir) {
|
|
72
|
-
break // Reached root
|
|
73
|
-
}
|
|
74
|
-
currentDir = parentDir
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let config: GlotstackConfig
|
|
78
|
-
|
|
79
|
-
if (configPath != null) {
|
|
80
|
-
console.info('Loading config file at ', configPath)
|
|
81
|
-
try {
|
|
82
|
-
const content = await readFile(configPath)
|
|
83
|
-
const text = content.toString()
|
|
84
|
-
if (path.parse(configPath).ext === '.yaml') {
|
|
85
|
-
config = loadYaml(text) as GlotstackConfig
|
|
86
|
-
} else {
|
|
87
|
-
config = JSON.parse(text)
|
|
88
|
-
}
|
|
89
|
-
console.info('Loaded config file', configPath, dropKeys(config, ["apiKey"]))
|
|
90
|
-
return config
|
|
91
|
-
} catch (err) {
|
|
92
|
-
console.warn('Could not load config', configPath)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
console.warn('Could not find any .glotstack config files')
|
|
96
|
-
return null
|
|
97
|
-
}
|
package/src/util/object.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
export function isObject(val: any): val is Record<string, any> {
|
|
2
|
-
return typeof val === 'object' && val !== null && !Array.isArray(val);
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function _merge(target: any, source: any): any {
|
|
6
|
-
if (!isObject(target) || !isObject(source)) return target;
|
|
7
|
-
|
|
8
|
-
for (const key of Object.keys(source)) {
|
|
9
|
-
const srcVal = source[key];
|
|
10
|
-
const tgtVal = target[key];
|
|
11
|
-
|
|
12
|
-
if (Array.isArray(srcVal) && Array.isArray(tgtVal)) {
|
|
13
|
-
target[key] = tgtVal.concat(srcVal);
|
|
14
|
-
} else if (isObject(srcVal) && isObject(tgtVal)) {
|
|
15
|
-
_merge(tgtVal, srcVal);
|
|
16
|
-
} else {
|
|
17
|
-
target[key] = srcVal;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return target;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function merge<T extends object>(target: T, ...sources: Partial<T>[]): T {
|
|
25
|
-
return sources.reduce((previous, current) => _merge(previous, current), target) as T
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function isEqual(a: any, b: any): boolean {
|
|
29
|
-
if (a === b) return true;
|
|
30
|
-
|
|
31
|
-
if (typeof a !== typeof b || a == null || b == null) return false;
|
|
32
|
-
|
|
33
|
-
if (Array.isArray(a)) {
|
|
34
|
-
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
35
|
-
return a.every((val, i) => isEqual(val, b[i]));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (typeof a === 'object') {
|
|
39
|
-
const aKeys = Object.keys(a);
|
|
40
|
-
const bKeys = Object.keys(b);
|
|
41
|
-
if (aKeys.length !== bKeys.length) return false;
|
|
42
|
-
|
|
43
|
-
return aKeys.every(key => b.hasOwnProperty(key) && isEqual(a[key], b[key]));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return false;
|
|
47
|
-
}
|
package/src/util/yaml.ts
DELETED
package/tsconfig.json
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"exclude": ["node_modules", "dist"],
|
|
3
|
-
"include": ["./src"],
|
|
4
|
-
"files": ["./types/global.d.ts"],
|
|
5
|
-
"compilerOptions": {
|
|
6
|
-
"outDir": "dist",
|
|
7
|
-
"target": "ES6",
|
|
8
|
-
"module": "CommonJS",
|
|
9
|
-
"declaration": true,
|
|
10
|
-
"strict": true,
|
|
11
|
-
"jsx": "react-jsx",
|
|
12
|
-
"sourceMap": true,
|
|
13
|
-
"baseUrl": ".",
|
|
14
|
-
"paths": {
|
|
15
|
-
},
|
|
16
|
-
"esModuleInterop": true,
|
|
17
|
-
"skipLibCheck": true,
|
|
18
|
-
"forceConsistentCasingInFileNames": true,
|
|
19
|
-
"noImplicitAny": true,
|
|
20
|
-
"noImplicitThis": true,
|
|
21
|
-
"strictNullChecks": true
|
|
22
|
-
}
|
|
23
|
-
}
|