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.
@@ -1 +1 @@
1
- {"version":3,"file":"findConfig.js","sourceRoot":"","sources":["../../src/util/findConfig.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2BAA+B;AAC/B,0CAAsC;AACtC,2CAA4B;AAE5B;;;;GAIG;AACH,SAAsB,mBAAmB;yDAAC,WAAmB,OAAO,CAAC,GAAG,EAAE;QACxE,IAAI,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;QACvC,IAAI,UAAU,GAAG,IAAI,CAAA;QAErB,OAAO,IAAI,EAAE,CAAC;YACZ,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAA;YAC1D,IAAI,IAAA,eAAU,EAAC,SAAS,CAAC,EAAE,CAAC;gBAC1B,UAAU,GAAG,SAAS,CAAA;YACxB,CAAC;YAED,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;YAC1C,IAAI,SAAS,KAAK,UAAU,EAAE,CAAC;gBAC7B,MAAK,CAAC,eAAe;YACvB,CAAC;YACD,UAAU,GAAG,SAAS,CAAA;QACxB,CAAC;QAED,IAAI,MAAM,GAAG,EAAE,CAAA;QAEf,IAAI,UAAU,IAAI,IAAI,EAAE,CAAC;YACvB,OAAO,CAAC,IAAI,CAAC,yBAAyB,EAAE,UAAU,CAAC,CAAA;YACnD,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,MAAM,IAAA,mBAAQ,EAAC,UAAU,EAAE,OAAO,CAAC,CAAA;gBAChD,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;gBACzB,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAA;gBAC1C,OAAO,MAAM,CAAA;YACf,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,CAAC,IAAI,CAAC,uBAAuB,EAAE,UAAU,CAAC,CAAA;YACnD,CAAC;QACH,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAA;QAC/D,OAAO,IAAI,CAAA;IACb,CAAC;CAAA;AAhCD,kDAgCC"}
1
+ {"version":3,"file":"findConfig.js","sourceRoot":"","sources":["../../src/util/findConfig.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,3 @@
1
1
  export declare function isObject(val: any): val is Record<string, any>;
2
- type MergeTarget = Record<string | number | symbol, any>;
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 {};
@@ -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) {
@@ -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;AAID,SAAgB,KAAK,CAAC,MAAmB,EAAE,GAAG,OAAsB;IAClE,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE;QAC1C,OAAO,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACnC,CAAC,EAAE,MAAM,CAAC,CAAA;AACZ,CAAC;AAJD,sBAIC;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"}
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.8",
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 'npm run build'\"",
39
- "prepublishOnly": "yarn run build",
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
- async function resolveConfigAndOptions(options: Record<string, any>) {
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
- 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
- }
82
+ async function resolveConfigAndOptions(options: Record<string, any>): Promise<GlotstackConfig & { yes?: boolean; yaml?: boolean }> {
83
83
 
84
- const resolved = merge({}, DEFAULT_OPTIONS, config, options)
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
- .action(async (inputOptions: Record<string, any>) => {
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
- const url = `${options.apiOrigin}/uploads/translations/extract`
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 absPath = path.resolve(resolved.sourcePath, `${resolved.sourceLocale}.json`)
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: resolved.outputLocales,
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: (key: string, options?: { locale?: LocaleRegion }) => string
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(`[level=${level} logLevel=${logLevel}][glotstack.ai]`, ...args)
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
- export const GlotstackProvider = ({ children, initialLocale, initialTranslations, onLocaleChange, onTranslationLoaded, importMethod, ssr}: GlotstackProviderProps) => {
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>|null>(initialTranslations || null)
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
- return {
379
+ const context: ContextType = {
156
380
  setLocale,
157
381
  translations: translationsRef.current ?? {},
158
382
  locale,
159
383
  importMethod,
160
384
  loadTranslations,
161
- t: (key: string, opts?: { locale?: LocaleRegion }) => {
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
- }, [locale, importMethod])
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
+ }