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/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
- }
@@ -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
- }
@@ -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
@@ -1,11 +0,0 @@
1
- import yaml from 'js-yaml';
2
-
3
-
4
- export function loadYaml(yamlString: string) {
5
- try {
6
- return yaml.load(yamlString);
7
- } catch (e) {
8
- console.error('Error converting yaml to JSON', e)
9
- throw new Error('Could not convert yaml to JSON')
10
- }
11
- }
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
- }
package/types/global.d.ts DELETED
@@ -1,7 +0,0 @@
1
- declare module '*.png'
2
- declare module '*.svg'
3
- declare module '*.jpeg'
4
- declare module '*.jpg'
5
- declare module '*.webp'
6
- declare module '*.lottie'
7
- declare module '*.json'