@formatjs/cli-lib 5.1.6 → 5.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BUILD +118 -0
- package/CHANGELOG.md +1147 -0
- package/index.ts +7 -0
- package/main.ts +5 -0
- package/package.json +4 -4
- package/src/cli.ts +240 -0
- package/src/compile.ts +141 -0
- package/src/compile_folder.ts +15 -0
- package/src/console_utils.ts +78 -0
- package/src/extract.ts +273 -0
- package/src/formatters/crowdin.ts +34 -0
- package/src/formatters/default.ts +19 -0
- package/src/formatters/index.ts +46 -0
- package/src/formatters/lokalise.ts +33 -0
- package/src/formatters/simple.ts +12 -0
- package/src/formatters/smartling.ts +73 -0
- package/src/formatters/transifex.ts +33 -0
- package/src/parse_script.ts +49 -0
- package/src/pseudo_locale.ts +113 -0
- package/src/vue_extractor.ts +96 -0
- package/tests/unit/__snapshots__/pseudo_locale.test.ts.snap +24 -0
- package/tests/unit/__snapshots__/unit.test.ts.snap +42 -0
- package/tests/unit/__snapshots__/vue_extractor.test.ts.snap +36 -0
- package/tests/unit/fixtures/bind.vue +46 -0
- package/tests/unit/fixtures/comp.vue +17 -0
- package/tests/unit/pseudo_locale.test.ts +7 -0
- package/tests/unit/unit.test.ts +44 -0
- package/tests/unit/vue_extractor.test.ts +38 -0
- package/tsconfig.json +5 -0
- package/index.d.ts +0 -8
- package/index.d.ts.map +0 -1
- package/index.js +0 -12
- package/main.d.ts +0 -2
- package/main.d.ts.map +0 -1
- package/main.js +0 -3
- package/src/cli.d.ts +0 -3
- package/src/cli.d.ts.map +0 -1
- package/src/cli.js +0 -165
- package/src/compile.d.ts +0 -48
- package/src/compile.d.ts.map +0 -1
- package/src/compile.js +0 -97
- package/src/compile_folder.d.ts +0 -3
- package/src/compile_folder.d.ts.map +0 -1
- package/src/compile_folder.js +0 -11
- package/src/console_utils.d.ts +0 -10
- package/src/console_utils.d.ts.map +0 -1
- package/src/console_utils.js +0 -76
- package/src/extract.d.ts +0 -75
- package/src/extract.d.ts.map +0 -1
- package/src/extract.js +0 -177
- package/src/formatters/crowdin.d.ts +0 -8
- package/src/formatters/crowdin.d.ts.map +0 -1
- package/src/formatters/crowdin.js +0 -27
- package/src/formatters/default.d.ts +0 -6
- package/src/formatters/default.d.ts.map +0 -1
- package/src/formatters/default.js +0 -13
- package/src/formatters/index.d.ts +0 -9
- package/src/formatters/index.d.ts.map +0 -1
- package/src/formatters/index.js +0 -42
- package/src/formatters/lokalise.d.ts +0 -10
- package/src/formatters/lokalise.d.ts.map +0 -1
- package/src/formatters/lokalise.js +0 -24
- package/src/formatters/simple.d.ts +0 -5
- package/src/formatters/simple.d.ts.map +0 -1
- package/src/formatters/simple.js +0 -12
- package/src/formatters/smartling.d.ts +0 -24
- package/src/formatters/smartling.d.ts.map +0 -1
- package/src/formatters/smartling.js +0 -50
- package/src/formatters/transifex.d.ts +0 -10
- package/src/formatters/transifex.d.ts.map +0 -1
- package/src/formatters/transifex.js +0 -24
- package/src/parse_script.d.ts +0 -8
- package/src/parse_script.d.ts.map +0 -1
- package/src/parse_script.js +0 -51
- package/src/pseudo_locale.d.ts +0 -7
- package/src/pseudo_locale.d.ts.map +0 -1
- package/src/pseudo_locale.js +0 -100
- package/src/vue_extractor.d.ts +0 -3
- package/src/vue_extractor.d.ts.map +0 -1
- package/src/vue_extractor.js +0 -62
package/src/extract.ts
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {warn, getStdinAsString, debug, writeStdout} from './console_utils'
|
|
2
|
+
import {readFile, outputFile} from 'fs-extra'
|
|
3
|
+
import {
|
|
4
|
+
interpolateName,
|
|
5
|
+
Opts,
|
|
6
|
+
MessageDescriptor,
|
|
7
|
+
} from '@formatjs/ts-transformer'
|
|
8
|
+
|
|
9
|
+
import {resolveBuiltinFormatter, Formatter} from './formatters'
|
|
10
|
+
import stringify from 'json-stable-stringify'
|
|
11
|
+
import {parseScript} from './parse_script'
|
|
12
|
+
import {printAST} from '@formatjs/icu-messageformat-parser/printer'
|
|
13
|
+
import {hoistSelectors} from '@formatjs/icu-messageformat-parser/manipulator'
|
|
14
|
+
import {parse} from '@formatjs/icu-messageformat-parser'
|
|
15
|
+
export interface ExtractionResult<M = Record<string, string>> {
|
|
16
|
+
/**
|
|
17
|
+
* List of extracted messages
|
|
18
|
+
*/
|
|
19
|
+
messages: MessageDescriptor[]
|
|
20
|
+
/**
|
|
21
|
+
* Metadata extracted w/ `pragma`
|
|
22
|
+
*/
|
|
23
|
+
meta?: M
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ExtractedMessageDescriptor extends MessageDescriptor {
|
|
27
|
+
/**
|
|
28
|
+
* Line number
|
|
29
|
+
*/
|
|
30
|
+
line?: number
|
|
31
|
+
/**
|
|
32
|
+
* Column number
|
|
33
|
+
*/
|
|
34
|
+
col?: number
|
|
35
|
+
/**
|
|
36
|
+
* Metadata extracted from pragma
|
|
37
|
+
*/
|
|
38
|
+
meta?: Record<string, string>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ExtractCLIOptions = Omit<
|
|
42
|
+
ExtractOpts,
|
|
43
|
+
'overrideIdFn' | 'onMsgExtracted' | 'onMetaExtracted'
|
|
44
|
+
> & {
|
|
45
|
+
/**
|
|
46
|
+
* Output File
|
|
47
|
+
*/
|
|
48
|
+
outFile?: string
|
|
49
|
+
/**
|
|
50
|
+
* Ignore file glob pattern
|
|
51
|
+
*/
|
|
52
|
+
ignore?: string[]
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ExtractOpts = Opts & {
|
|
56
|
+
/**
|
|
57
|
+
* Whether to throw an error if we had any issues with
|
|
58
|
+
* 1 of the source files
|
|
59
|
+
*/
|
|
60
|
+
throws?: boolean
|
|
61
|
+
/**
|
|
62
|
+
* Message ID interpolation pattern
|
|
63
|
+
*/
|
|
64
|
+
idInterpolationPattern?: string
|
|
65
|
+
/**
|
|
66
|
+
* Whether we read from stdin instead of a file
|
|
67
|
+
*/
|
|
68
|
+
readFromStdin?: boolean
|
|
69
|
+
/**
|
|
70
|
+
* Path to a formatter file that controls the shape of JSON file from `outFile`.
|
|
71
|
+
*/
|
|
72
|
+
format?: string | Formatter
|
|
73
|
+
/**
|
|
74
|
+
* Whether to hoist selectors & flatten sentences
|
|
75
|
+
*/
|
|
76
|
+
flatten?: boolean
|
|
77
|
+
} & Pick<Opts, 'onMsgExtracted' | 'onMetaExtracted'>
|
|
78
|
+
|
|
79
|
+
function calculateLineColFromOffset(
|
|
80
|
+
text: string,
|
|
81
|
+
start?: number
|
|
82
|
+
): Pick<ExtractedMessageDescriptor, 'line' | 'col'> {
|
|
83
|
+
if (!start) {
|
|
84
|
+
return {line: 1, col: 1}
|
|
85
|
+
}
|
|
86
|
+
const chunk = text.slice(0, start)
|
|
87
|
+
const lines = chunk.split('\n')
|
|
88
|
+
const lastLine = lines[lines.length - 1]
|
|
89
|
+
return {line: lines.length, col: lastLine.length}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function processFile(
|
|
93
|
+
source: string,
|
|
94
|
+
fn: string,
|
|
95
|
+
{idInterpolationPattern, ...opts}: Opts & {idInterpolationPattern?: string}
|
|
96
|
+
) {
|
|
97
|
+
let messages: ExtractedMessageDescriptor[] = []
|
|
98
|
+
let meta: Record<string, string> | undefined
|
|
99
|
+
|
|
100
|
+
opts = {
|
|
101
|
+
...opts,
|
|
102
|
+
additionalComponentNames: [
|
|
103
|
+
'$formatMessage',
|
|
104
|
+
...(opts.additionalComponentNames || []),
|
|
105
|
+
],
|
|
106
|
+
onMsgExtracted(_, msgs) {
|
|
107
|
+
if (opts.extractSourceLocation) {
|
|
108
|
+
msgs = msgs.map(msg => ({
|
|
109
|
+
...msg,
|
|
110
|
+
...calculateLineColFromOffset(source, msg.start),
|
|
111
|
+
}))
|
|
112
|
+
}
|
|
113
|
+
messages = messages.concat(msgs)
|
|
114
|
+
},
|
|
115
|
+
onMetaExtracted(_, m) {
|
|
116
|
+
meta = m
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!opts.overrideIdFn && idInterpolationPattern) {
|
|
121
|
+
opts = {
|
|
122
|
+
...opts,
|
|
123
|
+
overrideIdFn: (id, defaultMessage, description, fileName) =>
|
|
124
|
+
id ||
|
|
125
|
+
interpolateName(
|
|
126
|
+
{
|
|
127
|
+
resourcePath: fileName,
|
|
128
|
+
} as any,
|
|
129
|
+
idInterpolationPattern,
|
|
130
|
+
{
|
|
131
|
+
content: description
|
|
132
|
+
? `${defaultMessage}#${
|
|
133
|
+
typeof description === 'string'
|
|
134
|
+
? description
|
|
135
|
+
: stringify(description)
|
|
136
|
+
}`
|
|
137
|
+
: defaultMessage,
|
|
138
|
+
}
|
|
139
|
+
),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
debug('Processing opts for %s: %s', fn, opts)
|
|
144
|
+
|
|
145
|
+
const scriptParseFn = parseScript(opts, fn)
|
|
146
|
+
if (fn.endsWith('.vue')) {
|
|
147
|
+
debug('Processing %s using vue extractor', fn)
|
|
148
|
+
const {parseFile} = await import('./vue_extractor')
|
|
149
|
+
parseFile(source, fn, scriptParseFn)
|
|
150
|
+
} else {
|
|
151
|
+
debug('Processing %s using typescript extractor', fn)
|
|
152
|
+
scriptParseFn(source)
|
|
153
|
+
}
|
|
154
|
+
debug('Done extracting %s messages: %s', fn, messages)
|
|
155
|
+
if (meta) {
|
|
156
|
+
debug('Extracted meta:', meta)
|
|
157
|
+
messages.forEach(m => (m.meta = meta))
|
|
158
|
+
}
|
|
159
|
+
return {messages, meta}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Extract strings from source files
|
|
164
|
+
* @param files list of files
|
|
165
|
+
* @param extractOpts extract options
|
|
166
|
+
* @returns messages serialized as JSON string since key order
|
|
167
|
+
* matters for some `format`
|
|
168
|
+
*/
|
|
169
|
+
export async function extract(
|
|
170
|
+
files: readonly string[],
|
|
171
|
+
extractOpts: ExtractOpts
|
|
172
|
+
) {
|
|
173
|
+
const {throws, readFromStdin, flatten, ...opts} = extractOpts
|
|
174
|
+
let rawResults: Array<ExtractionResult | undefined>
|
|
175
|
+
if (readFromStdin) {
|
|
176
|
+
debug(`Reading input from stdin`)
|
|
177
|
+
// Read from stdin
|
|
178
|
+
if (process.stdin.isTTY) {
|
|
179
|
+
warn('Reading source file from TTY.')
|
|
180
|
+
}
|
|
181
|
+
const stdinSource = await getStdinAsString()
|
|
182
|
+
rawResults = [await processFile(stdinSource, 'dummy', opts)]
|
|
183
|
+
} else {
|
|
184
|
+
rawResults = await Promise.all(
|
|
185
|
+
files.map(async fn => {
|
|
186
|
+
debug('Extracting file:', fn)
|
|
187
|
+
try {
|
|
188
|
+
const source = await readFile(fn, 'utf8')
|
|
189
|
+
return processFile(source, fn, opts)
|
|
190
|
+
} catch (e) {
|
|
191
|
+
if (throws) {
|
|
192
|
+
throw e
|
|
193
|
+
} else {
|
|
194
|
+
warn(String(e))
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const formatter = await resolveBuiltinFormatter(opts.format)
|
|
202
|
+
const extractionResults = rawResults.filter((r): r is ExtractionResult => !!r)
|
|
203
|
+
|
|
204
|
+
const extractedMessages = new Map<string, MessageDescriptor>()
|
|
205
|
+
|
|
206
|
+
for (const {messages} of extractionResults) {
|
|
207
|
+
for (const message of messages) {
|
|
208
|
+
const {id, description, defaultMessage} = message
|
|
209
|
+
if (!id) {
|
|
210
|
+
const error = new Error(
|
|
211
|
+
`[FormatJS CLI] Missing message id for message:
|
|
212
|
+
${JSON.stringify(message, undefined, 2)}`
|
|
213
|
+
)
|
|
214
|
+
if (throws) {
|
|
215
|
+
throw error
|
|
216
|
+
} else {
|
|
217
|
+
warn(error.message)
|
|
218
|
+
}
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (extractedMessages.has(id)) {
|
|
223
|
+
const existing = extractedMessages.get(id)!
|
|
224
|
+
if (
|
|
225
|
+
stringify(description) !== stringify(existing.description) ||
|
|
226
|
+
defaultMessage !== existing.defaultMessage
|
|
227
|
+
) {
|
|
228
|
+
const error = new Error(
|
|
229
|
+
`[FormatJS CLI] Duplicate message id: "${id}", ` +
|
|
230
|
+
'but the `description` and/or `defaultMessage` are different.'
|
|
231
|
+
)
|
|
232
|
+
if (throws) {
|
|
233
|
+
throw error
|
|
234
|
+
} else {
|
|
235
|
+
warn(error.message)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
extractedMessages.set(id, message)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const results: Record<string, Omit<MessageDescriptor, 'id'>> = {}
|
|
243
|
+
const messages = Array.from(extractedMessages.values())
|
|
244
|
+
for (const {id, ...msg} of messages) {
|
|
245
|
+
if (flatten && msg.defaultMessage) {
|
|
246
|
+
msg.defaultMessage = printAST(hoistSelectors(parse(msg.defaultMessage)))
|
|
247
|
+
}
|
|
248
|
+
results[id] = msg
|
|
249
|
+
}
|
|
250
|
+
return stringify(formatter.format(results), {
|
|
251
|
+
space: 2,
|
|
252
|
+
cmp: formatter.compareMessages || undefined,
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Extract strings from source files, also writes to a file.
|
|
258
|
+
* @param files list of files
|
|
259
|
+
* @param extractOpts extract options
|
|
260
|
+
* @returns A Promise that resolves if output file was written successfully
|
|
261
|
+
*/
|
|
262
|
+
export default async function extractAndWrite(
|
|
263
|
+
files: readonly string[],
|
|
264
|
+
extractOpts: ExtractCLIOptions
|
|
265
|
+
) {
|
|
266
|
+
const {outFile, ...opts} = extractOpts
|
|
267
|
+
const serializedResult = (await extract(files, opts)) + '\n'
|
|
268
|
+
if (outFile) {
|
|
269
|
+
debug('Writing output file:', outFile)
|
|
270
|
+
return outputFile(outFile, serializedResult)
|
|
271
|
+
}
|
|
272
|
+
await writeStdout(serializedResult)
|
|
273
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {CompileFn, FormatFn} from './default'
|
|
2
|
+
|
|
3
|
+
export type SmartlingJson = Record<
|
|
4
|
+
string,
|
|
5
|
+
{
|
|
6
|
+
message: string
|
|
7
|
+
description?: string
|
|
8
|
+
}
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
export const format: FormatFn<SmartlingJson> = msgs => {
|
|
12
|
+
const results: SmartlingJson = {}
|
|
13
|
+
for (const [id, msg] of Object.entries(msgs)) {
|
|
14
|
+
results[id] = {
|
|
15
|
+
message: msg.defaultMessage!,
|
|
16
|
+
description:
|
|
17
|
+
typeof msg.description === 'string'
|
|
18
|
+
? msg.description
|
|
19
|
+
: JSON.stringify(msg.description),
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return results
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const compile: CompileFn<SmartlingJson> = msgs => {
|
|
26
|
+
const results: Record<string, string> = {}
|
|
27
|
+
for (const [id, msg] of Object.entries(msgs)) {
|
|
28
|
+
if (id === 'smartling') {
|
|
29
|
+
continue
|
|
30
|
+
}
|
|
31
|
+
results[id] = msg.message
|
|
32
|
+
}
|
|
33
|
+
return results
|
|
34
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {MessageDescriptor} from '@formatjs/ts-transformer'
|
|
2
|
+
|
|
3
|
+
export type FormatFn<T = Record<string, MessageDescriptor>> = (
|
|
4
|
+
msgs: Record<string, MessageDescriptor>
|
|
5
|
+
) => T
|
|
6
|
+
|
|
7
|
+
export type CompileFn<T = Record<string, MessageDescriptor>> = (
|
|
8
|
+
msgs: T
|
|
9
|
+
) => Record<string, string>
|
|
10
|
+
|
|
11
|
+
export const format: FormatFn = msgs => msgs
|
|
12
|
+
|
|
13
|
+
export const compile: CompileFn = msgs => {
|
|
14
|
+
const results: Record<string, string> = {}
|
|
15
|
+
for (const k in msgs) {
|
|
16
|
+
results[k] = msgs[k].defaultMessage!
|
|
17
|
+
}
|
|
18
|
+
return results
|
|
19
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as defaultFormatter from './default'
|
|
2
|
+
import {FormatFn, CompileFn} from './default'
|
|
3
|
+
import * as transifex from './transifex'
|
|
4
|
+
import * as smartling from './smartling'
|
|
5
|
+
import * as simple from './simple'
|
|
6
|
+
import * as lokalise from './lokalise'
|
|
7
|
+
import * as crowdin from './crowdin'
|
|
8
|
+
import {Comparator} from 'json-stable-stringify'
|
|
9
|
+
import {resolve} from 'path'
|
|
10
|
+
import {pathToFileURL} from 'url'
|
|
11
|
+
|
|
12
|
+
export interface Formatter {
|
|
13
|
+
format: FormatFn
|
|
14
|
+
compile: CompileFn
|
|
15
|
+
compareMessages?: Comparator
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function resolveBuiltinFormatter(
|
|
19
|
+
format?: string | Formatter
|
|
20
|
+
): Promise<any> {
|
|
21
|
+
if (!format) {
|
|
22
|
+
return defaultFormatter
|
|
23
|
+
}
|
|
24
|
+
if (typeof format !== 'string') {
|
|
25
|
+
return format
|
|
26
|
+
}
|
|
27
|
+
switch (format) {
|
|
28
|
+
case 'transifex':
|
|
29
|
+
return transifex
|
|
30
|
+
case 'smartling':
|
|
31
|
+
return smartling
|
|
32
|
+
case 'simple':
|
|
33
|
+
return simple
|
|
34
|
+
case 'lokalise':
|
|
35
|
+
return lokalise
|
|
36
|
+
case 'crowdin':
|
|
37
|
+
return crowdin
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
// eslint-disable-next-line import/dynamic-import-chunkname
|
|
41
|
+
return import(pathToFileURL(resolve(process.cwd(), format)).href)
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error(`Cannot resolve formatter ${format}`)
|
|
44
|
+
throw e
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {CompileFn, FormatFn} from './default'
|
|
2
|
+
|
|
3
|
+
export type StructuredJson = Record<
|
|
4
|
+
string,
|
|
5
|
+
{
|
|
6
|
+
translation: string
|
|
7
|
+
notes?: string
|
|
8
|
+
context?: string
|
|
9
|
+
limit?: string
|
|
10
|
+
}
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
export const format: FormatFn<StructuredJson> = msgs => {
|
|
14
|
+
const results: StructuredJson = {}
|
|
15
|
+
for (const [id, msg] of Object.entries(msgs)) {
|
|
16
|
+
results[id] = {
|
|
17
|
+
translation: msg.defaultMessage!,
|
|
18
|
+
notes:
|
|
19
|
+
typeof msg.description === 'string'
|
|
20
|
+
? msg.description
|
|
21
|
+
: JSON.stringify(msg.description),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return results
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const compile: CompileFn<StructuredJson> = msgs => {
|
|
28
|
+
const results: Record<string, string> = {}
|
|
29
|
+
for (const [id, msg] of Object.entries(msgs)) {
|
|
30
|
+
results[id] = msg.translation
|
|
31
|
+
}
|
|
32
|
+
return results
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {CompileFn, FormatFn} from './default'
|
|
2
|
+
|
|
3
|
+
export type PhraseJson = Record<string, string>
|
|
4
|
+
|
|
5
|
+
export const format: FormatFn<PhraseJson> = msgs => {
|
|
6
|
+
return Object.keys(msgs).reduce((all: PhraseJson, k) => {
|
|
7
|
+
all[k] = msgs[k].defaultMessage!
|
|
8
|
+
return all
|
|
9
|
+
}, {})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const compile: CompileFn<PhraseJson> = msgs => msgs
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {Comparator} from 'json-stable-stringify'
|
|
2
|
+
import {CompileFn, FormatFn} from './default'
|
|
3
|
+
|
|
4
|
+
export interface SmartlingDirectives {
|
|
5
|
+
translate_paths: [
|
|
6
|
+
{
|
|
7
|
+
path: string
|
|
8
|
+
key: string
|
|
9
|
+
instruction: string
|
|
10
|
+
}
|
|
11
|
+
]
|
|
12
|
+
variants_enabled: boolean
|
|
13
|
+
string_format: string
|
|
14
|
+
[k: string]: any
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SmartlingJson = {
|
|
18
|
+
smartling: SmartlingDirectives
|
|
19
|
+
} & Record<
|
|
20
|
+
string,
|
|
21
|
+
{
|
|
22
|
+
message: string
|
|
23
|
+
description?: string
|
|
24
|
+
}
|
|
25
|
+
>
|
|
26
|
+
|
|
27
|
+
export const format: FormatFn<SmartlingJson> = msgs => {
|
|
28
|
+
const results: SmartlingJson = {
|
|
29
|
+
smartling: {
|
|
30
|
+
translate_paths: [
|
|
31
|
+
{
|
|
32
|
+
path: '*/message',
|
|
33
|
+
key: '{*}/message',
|
|
34
|
+
instruction: '*/description',
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
variants_enabled: true,
|
|
38
|
+
string_format: 'icu',
|
|
39
|
+
},
|
|
40
|
+
} as any
|
|
41
|
+
for (const [id, msg] of Object.entries(msgs)) {
|
|
42
|
+
results[id] = {
|
|
43
|
+
message: msg.defaultMessage!,
|
|
44
|
+
description:
|
|
45
|
+
typeof msg.description === 'string'
|
|
46
|
+
? msg.description
|
|
47
|
+
: JSON.stringify(msg.description),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return results
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const compareMessages: Comparator = (el1, el2) => {
|
|
54
|
+
// `smartling` has to be the 1st key
|
|
55
|
+
if (el1.key === 'smartling') {
|
|
56
|
+
return -1
|
|
57
|
+
}
|
|
58
|
+
if (el2.key === 'smartling') {
|
|
59
|
+
return 1
|
|
60
|
+
}
|
|
61
|
+
return el1.key < el2.key ? -1 : el1.key === el2.key ? 0 : 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const compile: CompileFn<SmartlingJson> = msgs => {
|
|
65
|
+
const results: Record<string, string> = {}
|
|
66
|
+
for (const [id, msg] of Object.entries(msgs)) {
|
|
67
|
+
if (id === 'smartling') {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
results[id] = msg.message
|
|
71
|
+
}
|
|
72
|
+
return results
|
|
73
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {CompileFn, FormatFn} from './default'
|
|
2
|
+
|
|
3
|
+
export type StructuredJson = Record<
|
|
4
|
+
string,
|
|
5
|
+
{
|
|
6
|
+
string: string
|
|
7
|
+
developer_comment?: string
|
|
8
|
+
context?: string
|
|
9
|
+
character_limit?: string
|
|
10
|
+
}
|
|
11
|
+
>
|
|
12
|
+
|
|
13
|
+
export const format: FormatFn<StructuredJson> = msgs => {
|
|
14
|
+
const results: StructuredJson = {}
|
|
15
|
+
for (const [id, msg] of Object.entries(msgs)) {
|
|
16
|
+
results[id] = {
|
|
17
|
+
string: msg.defaultMessage!,
|
|
18
|
+
developer_comment:
|
|
19
|
+
typeof msg.description === 'string'
|
|
20
|
+
? msg.description
|
|
21
|
+
: JSON.stringify(msg.description),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return results
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const compile: CompileFn<StructuredJson> = msgs => {
|
|
28
|
+
const results: Record<string, string> = {}
|
|
29
|
+
for (const [id, msg] of Object.entries(msgs)) {
|
|
30
|
+
results[id] = msg.string
|
|
31
|
+
}
|
|
32
|
+
return results
|
|
33
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import {Opts, transformWithTs} from '@formatjs/ts-transformer'
|
|
2
|
+
import ts from 'typescript'
|
|
3
|
+
import {debug} from './console_utils'
|
|
4
|
+
/**
|
|
5
|
+
* Invoid TypeScript module transpilation with our TS transformer
|
|
6
|
+
* @param opts Formatjs TS Transformer opt
|
|
7
|
+
* @param fn filename
|
|
8
|
+
*/
|
|
9
|
+
export function parseScript(opts: Opts, fn?: string) {
|
|
10
|
+
return (source: string) => {
|
|
11
|
+
let output
|
|
12
|
+
try {
|
|
13
|
+
debug('Using TS compiler to process file', fn)
|
|
14
|
+
output = ts.transpileModule(source, {
|
|
15
|
+
compilerOptions: {
|
|
16
|
+
allowJs: true,
|
|
17
|
+
target: ts.ScriptTarget.ESNext,
|
|
18
|
+
noEmit: true,
|
|
19
|
+
experimentalDecorators: true,
|
|
20
|
+
},
|
|
21
|
+
reportDiagnostics: true,
|
|
22
|
+
fileName: fn,
|
|
23
|
+
transformers: {
|
|
24
|
+
before: [transformWithTs(ts, opts)],
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
} catch (e) {
|
|
28
|
+
if (e instanceof Error) {
|
|
29
|
+
e.message = `Error processing file ${fn}
|
|
30
|
+
${e.message || ''}`
|
|
31
|
+
}
|
|
32
|
+
throw e
|
|
33
|
+
}
|
|
34
|
+
if (output.diagnostics) {
|
|
35
|
+
const errs = output.diagnostics.filter(
|
|
36
|
+
d => d.category === ts.DiagnosticCategory.Error
|
|
37
|
+
)
|
|
38
|
+
if (errs.length) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
ts.formatDiagnosticsWithColorAndContext(errs, {
|
|
41
|
+
getCanonicalFileName: fileName => fileName,
|
|
42
|
+
getCurrentDirectory: () => process.cwd(),
|
|
43
|
+
getNewLine: () => ts.sys.newLine,
|
|
44
|
+
})
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parse,
|
|
3
|
+
MessageFormatElement,
|
|
4
|
+
TYPE,
|
|
5
|
+
isLiteralElement,
|
|
6
|
+
isPluralElement,
|
|
7
|
+
isSelectElement,
|
|
8
|
+
isTagElement,
|
|
9
|
+
} from '@formatjs/icu-messageformat-parser'
|
|
10
|
+
|
|
11
|
+
export function generateXXLS(
|
|
12
|
+
msg: string | MessageFormatElement[]
|
|
13
|
+
): MessageFormatElement[] {
|
|
14
|
+
const ast = typeof msg === 'string' ? parse(msg) : msg
|
|
15
|
+
const lastChunk = ast[ast.length - 1]
|
|
16
|
+
if (lastChunk && isLiteralElement(lastChunk)) {
|
|
17
|
+
lastChunk.value += 'SSSSSSSSSSSSSSSSSSSSSSSSS'
|
|
18
|
+
return ast
|
|
19
|
+
}
|
|
20
|
+
return [...ast, {type: TYPE.literal, value: 'SSSSSSSSSSSSSSSSSSSSSSSSS'}]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function generateXXAC(
|
|
24
|
+
msg: string | MessageFormatElement[]
|
|
25
|
+
): MessageFormatElement[] {
|
|
26
|
+
const ast = typeof msg === 'string' ? parse(msg) : msg
|
|
27
|
+
ast.forEach(el => {
|
|
28
|
+
if (isLiteralElement(el)) {
|
|
29
|
+
el.value = el.value.toUpperCase()
|
|
30
|
+
} else if (isPluralElement(el) || isSelectElement(el)) {
|
|
31
|
+
for (const opt of Object.values(el.options)) {
|
|
32
|
+
generateXXAC(opt.value)
|
|
33
|
+
}
|
|
34
|
+
} else if (isTagElement(el)) {
|
|
35
|
+
generateXXAC(el.children)
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
return ast
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function generateXXHA(
|
|
42
|
+
msg: string | MessageFormatElement[]
|
|
43
|
+
): MessageFormatElement[] {
|
|
44
|
+
const ast = typeof msg === 'string' ? parse(msg) : msg
|
|
45
|
+
const firstChunk = ast.shift()
|
|
46
|
+
if (firstChunk && isLiteralElement(firstChunk)) {
|
|
47
|
+
firstChunk.value = '[javascript]' + firstChunk.value
|
|
48
|
+
return [firstChunk, ...ast]
|
|
49
|
+
}
|
|
50
|
+
return [{type: TYPE.literal, value: '[javascript]'}, ...ast]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const ASCII = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
54
|
+
const ACCENTED_ASCII = 'âḃćḋèḟĝḫíĵǩĺṁńŏṗɋŕśṭůṿẘẋẏẓḀḂḈḊḔḞḠḢḬĴḴĻḾŊÕṔɊŔṠṮŨṼẄẌŸƵ'
|
|
55
|
+
|
|
56
|
+
export function generateENXA(
|
|
57
|
+
msg: string | MessageFormatElement[]
|
|
58
|
+
): MessageFormatElement[] {
|
|
59
|
+
const ast = typeof msg === 'string' ? parse(msg) : msg
|
|
60
|
+
ast.forEach(el => {
|
|
61
|
+
if (isLiteralElement(el)) {
|
|
62
|
+
el.value = el.value
|
|
63
|
+
.split('')
|
|
64
|
+
.map(c => {
|
|
65
|
+
const i = ASCII.indexOf(c)
|
|
66
|
+
if (i < 0) {
|
|
67
|
+
return c
|
|
68
|
+
}
|
|
69
|
+
return ACCENTED_ASCII[i]
|
|
70
|
+
})
|
|
71
|
+
.join('')
|
|
72
|
+
} else if (isPluralElement(el) || isSelectElement(el)) {
|
|
73
|
+
for (const opt of Object.values(el.options)) {
|
|
74
|
+
generateENXA(opt.value)
|
|
75
|
+
}
|
|
76
|
+
} else if (isTagElement(el)) {
|
|
77
|
+
generateENXA(el.children)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
return ast
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function generateENXB(
|
|
84
|
+
msg: string | MessageFormatElement[]
|
|
85
|
+
): MessageFormatElement[] {
|
|
86
|
+
const ast = typeof msg === 'string' ? parse(msg) : msg
|
|
87
|
+
ast.forEach(el => {
|
|
88
|
+
if (isLiteralElement(el)) {
|
|
89
|
+
const pseudoString = el.value
|
|
90
|
+
.split('')
|
|
91
|
+
.map((c, index) => {
|
|
92
|
+
const i = ASCII.indexOf(c)
|
|
93
|
+
const canPad = (index + 1) % 3 === 0
|
|
94
|
+
|
|
95
|
+
if (i < 0) {
|
|
96
|
+
return c
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return canPad ? ACCENTED_ASCII[i].repeat(3) : ACCENTED_ASCII[i]
|
|
100
|
+
})
|
|
101
|
+
.join('')
|
|
102
|
+
|
|
103
|
+
el.value = `[!! ${pseudoString} !!]`
|
|
104
|
+
} else if (isPluralElement(el) || isSelectElement(el)) {
|
|
105
|
+
for (const opt of Object.values(el.options)) {
|
|
106
|
+
generateENXB(opt.value)
|
|
107
|
+
}
|
|
108
|
+
} else if (isTagElement(el)) {
|
|
109
|
+
generateENXB(el.children)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
return ast
|
|
113
|
+
}
|