Skip to content

Commit f63da57

Browse files
authored
export command feature (#56)
* export command feature * remove normalize option
1 parent 6abb989 commit f63da57

File tree

3 files changed

+397
-0
lines changed

3 files changed

+397
-0
lines changed

src/commands/export.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
import { promisify } from 'util'
4+
import { Arguments, Argv } from 'yargs'
5+
6+
import { debug as Debug } from 'debug'
7+
const debug = Debug('vue-i18n-locale-message:commands:export')
8+
9+
const mkdirPromisify = promisify(fs.mkdir)
10+
const writeFilePromisify = promisify(fs.writeFile)
11+
12+
import {
13+
resolveProviderConf,
14+
loadProvider,
15+
loadProviderConf,
16+
DEFUALT_CONF
17+
} from '../utils'
18+
import { Locale, RawLocaleMessage } from '../../types'
19+
20+
type ExportOptions = {
21+
provider: string
22+
conf?: string
23+
output: string
24+
locales?: string
25+
format: string
26+
dryRun: boolean
27+
}
28+
29+
export const command = 'export'
30+
export const aliases = 'ex'
31+
export const describe = 'export locale messages from localization service'
32+
33+
export const builder = (args: Argv): Argv<ExportOptions> => {
34+
return args
35+
.option('provider', {
36+
type: 'string',
37+
alias: 'p',
38+
describe: 'the target localization service provider',
39+
demandOption: true
40+
})
41+
.option('conf', {
42+
type: 'string',
43+
alias: 'c',
44+
describe: 'the json file configration of localization service provider. If omitted, use the suffix file name with `-conf` for provider name of --provider (e.g. <provider>-conf.json).'
45+
})
46+
.option('output', {
47+
type: 'string',
48+
alias: 'o',
49+
describe: 'the path to output that exported locale messages',
50+
demandOption: true
51+
})
52+
.option('locales', {
53+
type: 'string',
54+
alias: 'l',
55+
default: '',
56+
describe: `option for some locales of locale messages, you can also be specified multi locale with comma delimiter. if it's not specified export all locale messages`
57+
})
58+
.option('format', {
59+
type: 'string',
60+
alias: 'f',
61+
default: 'json',
62+
describe: 'option for the locale messages format, default `json`'
63+
})
64+
.option('dryRun', {
65+
type: 'boolean',
66+
alias: 'd',
67+
default: false,
68+
describe: 'run the export command, but do not export to locale messages of localization service'
69+
})
70+
}
71+
72+
export const handler = async (args: Arguments<ExportOptions>): Promise<unknown> => {
73+
const { dryRun, format } = args
74+
const ProviderFactory = loadProvider(args.provider)
75+
76+
if (ProviderFactory === null) {
77+
// TODO: should refactor console message
78+
console.log(`Not found ${args.provider} provider`)
79+
return
80+
}
81+
82+
if (!args.output) {
83+
// TODO: should refactor console message
84+
console.log('You need to specify --output')
85+
return
86+
}
87+
88+
const confPath = resolveProviderConf(args.provider, args.conf)
89+
const conf = loadProviderConf(confPath) || DEFUALT_CONF
90+
91+
try {
92+
const locales = args.locales?.split(',').filter(p => p) as Locale[] || []
93+
const provider = ProviderFactory(conf)
94+
const messages = await provider.export({ locales, dryRun, format })
95+
await writeRawLocaleMessages(args.output, format, messages, args.dryRun)
96+
// TODO: should refactor console message
97+
console.log('export success')
98+
} catch (e) {
99+
// TODO: should refactor console message
100+
console.error('export fail', e)
101+
}
102+
}
103+
104+
async function writeRawLocaleMessages (output: string, format: string, messages: RawLocaleMessage[], dryRun: boolean) {
105+
debug('writeRawLocaleMessages', messages, dryRun)
106+
107+
// wrap mkdir with dryRun
108+
const mkdir = async (output: string) => {
109+
return !dryRun
110+
? mkdirPromisify(path.resolve(output), { recursive: true })
111+
: Promise.resolve()
112+
}
113+
114+
// wrap writeFile with dryRun
115+
const writeFile = async (output: string, format: string, message: RawLocaleMessage) => {
116+
const localePath = path.resolve(output, `${message.locale}.${format}`)
117+
console.log(`write '${message.locale}' messages to ${localePath}`)
118+
return !dryRun
119+
? writeFilePromisify(localePath, message.data)
120+
: Promise.resolve()
121+
}
122+
123+
// run!
124+
await mkdir(output)
125+
for (const message of messages) {
126+
await writeFile(output, format, message)
127+
}
128+
}
129+
130+
export default {
131+
command,
132+
aliases,
133+
describe,
134+
builder,
135+
handler
136+
}

test/commands/export.test.ts

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
import * as yargs from 'yargs'
2+
import * as path from 'path'
3+
4+
// ------
5+
// mocks
6+
7+
// l10n service provider module
8+
const mockExport = jest.fn()
9+
jest.mock('@scope/l10n-service-provider', () => {
10+
return jest.fn().mockImplementation(() => {
11+
return { export: mockExport }
12+
})
13+
})
14+
import L10nServiceProvider from '@scope/l10n-service-provider'
15+
16+
jest.mock('@scope/l10n-omit-service-provider', () => {
17+
return jest.fn().mockImplementation(() => {
18+
return {
19+
export: jest.fn().mockImplementation((locales, format) => {
20+
const data = [{
21+
locale: 'en',
22+
data: Buffer.from(JSON.stringify({ hello: 'hello' }))
23+
}, {
24+
locale: 'ja',
25+
data: Buffer.from(JSON.stringify({ hello: 'こんにちわわわ!' }))
26+
}]
27+
return Promise.resolve(data)
28+
})
29+
}
30+
})
31+
})
32+
import L10nOmitServiceProvider from '@scope/l10n-omit-service-provider'
33+
34+
// fs module
35+
jest.mock('fs', () => ({
36+
...jest.requireActual('fs'),
37+
mkdir: jest.fn(),
38+
writeFile: jest.fn()
39+
}))
40+
import fs from 'fs'
41+
42+
// --------------------
43+
// setup/teadown hooks
44+
45+
const PROCESS_CWD_TARGET_PATH = path.resolve(__dirname)
46+
47+
let orgCwd // for process.cwd mock
48+
let spyLog
49+
let spyError
50+
beforeEach(() => {
51+
spyLog = jest.spyOn(global.console, 'log')
52+
spyError = jest.spyOn(global.console, 'error')
53+
orgCwd = process.cwd
54+
process.cwd = jest.fn(() => PROCESS_CWD_TARGET_PATH) // mock: process.cwd
55+
})
56+
57+
afterEach(() => {
58+
spyError.mockRestore()
59+
spyLog.mockRestore()
60+
jest.clearAllMocks()
61+
process.cwd = orgCwd
62+
})
63+
64+
// -----------
65+
// test cases
66+
67+
test('require options', async () => {
68+
const exp = await import('../../src/commands/export')
69+
const cmd = yargs.command(exp)
70+
try {
71+
await new Promise((resolve, reject) => {
72+
cmd.parse(`export`, (err, argv, output) => {
73+
err ? reject(err) : resolve(output)
74+
})
75+
})
76+
} catch (e) {
77+
expect(e).toMatchObject({ name: 'YError' })
78+
}
79+
})
80+
81+
test('--provider: not found', async () => {
82+
const exp = await import('../../src/commands/export')
83+
const cmd = yargs.command(exp)
84+
await new Promise((resolve, reject) => {
85+
cmd.parse(`export --provider=./404-provider.js \
86+
--output=./foo`, (err, argv, output) => {
87+
err ? reject(err) : resolve(output)
88+
})
89+
})
90+
expect(spyLog).toHaveBeenCalledWith('Not found ./404-provider.js provider')
91+
})
92+
93+
test('--conf option', async () => {
94+
// setup mocks
95+
const data = [
96+
{ locale: 'ja', data: Buffer.from(JSON.stringify({})) },
97+
{ locale: 'en', data: Buffer.from(JSON.stringify({})) }
98+
]
99+
mockExport.mockImplementation(({ locales, format }) => Promise.resolve(data))
100+
101+
// run
102+
const exp = await import('../../src/commands/export')
103+
const cmd = yargs.command(exp)
104+
await new Promise((resolve, reject) => {
105+
cmd.parse(`export --provider=@scope/l10n-service-provider \
106+
--conf=./test/fixtures/conf/l10n-service-provider-conf.json \
107+
--output=./test/fixtures/locales \
108+
--dry-run`, (err, argv, output) => {
109+
err ? reject(err) : resolve(output)
110+
})
111+
})
112+
113+
expect(L10nServiceProvider).toHaveBeenCalledWith({
114+
provider: { token: 'xxx' }
115+
})
116+
})
117+
118+
test('--conf option omit', async () => {
119+
// run
120+
const exp = await import('../../src/commands/export')
121+
const cmd = yargs.command(exp)
122+
await new Promise((resolve, reject) => {
123+
cmd.parse(`export --provider=@scope/l10n-omit-service-provider \
124+
--output=./test/fixtures/locales \
125+
--dry-run`, (err, argv, output) => {
126+
err ? reject(err) : resolve(output)
127+
})
128+
})
129+
130+
expect(L10nOmitServiceProvider).toHaveBeenCalledWith({
131+
provider: { token: 'yyy' }
132+
})
133+
})
134+
135+
test('--locales option', async () => {
136+
// setup mocks
137+
const data = [
138+
{ locale: 'ja', data: Buffer.from(JSON.stringify({})) },
139+
{ locale: 'en', data: Buffer.from(JSON.stringify({})) }
140+
]
141+
mockExport.mockImplementation(({ locales, format }) => Promise.resolve(data))
142+
143+
// run
144+
const exp = await import('../../src/commands/export')
145+
const cmd = yargs.command(exp)
146+
await new Promise((resolve, reject) => {
147+
cmd.parse(`export --provider=@scope/l10n-service-provider \
148+
--output=./test/fixtures/locales \
149+
--locales=en,ja,fr \
150+
--dry-run`, (err, argv, output) => {
151+
err ? reject(err) : resolve(output)
152+
})
153+
})
154+
155+
expect(mockExport).toHaveBeenCalledWith({
156+
locales: ['en', 'ja', 'fr'],
157+
format: 'json',
158+
dryRun: true,
159+
normalize: undefined
160+
})
161+
})
162+
163+
test('--output option', async () => {
164+
// setup mocks
165+
const data = [
166+
{ locale: 'ja', data: Buffer.from(JSON.stringify({ hello: 'hello' })) },
167+
{ locale: 'en', data: Buffer.from(JSON.stringify({ hello: 'こんにちわわわ!' })) }
168+
]
169+
mockExport.mockImplementation(({ locales, format }) => Promise.resolve(data))
170+
const mockFS = fs as jest.Mocked<typeof fs>
171+
mockFS.mkdir.mockImplementation((p, option, cb) => cb(null))
172+
mockFS.writeFile.mockImplementation((p, data, cb) => cb(null))
173+
174+
// run
175+
const OUTPUT_FULL_PATH = path.resolve('./test/fixtures/locales')
176+
const exp = await import('../../src/commands/export')
177+
const cmd = yargs.command(exp)
178+
await new Promise((resolve, reject) => {
179+
cmd.parse(`export --provider=@scope/l10n-service-provider \
180+
--conf=./test/fixtures/conf/l10n-service-provider-conf.json \
181+
--output=./test/fixtures/locales`, (err, argv, output) => {
182+
err ? reject(err) : resolve(output)
183+
})
184+
})
185+
186+
expect(mockFS.mkdir.mock.calls[0][0]).toEqual(OUTPUT_FULL_PATH)
187+
expect(mockFS.mkdir.mock.calls[0][1]).toEqual({ recursive: true })
188+
})
189+
190+
test('--normalize option', async () => {
191+
// setup mocks
192+
const data = [
193+
{ locale: 'ja', data: Buffer.from(JSON.stringify({})) },
194+
{ locale: 'en', data: Buffer.from(JSON.stringify({})) }
195+
]
196+
mockExport.mockImplementation(({ locales, format }) => Promise.resolve(data))
197+
198+
// run
199+
const exp = await import('../../src/commands/export')
200+
const cmd = yargs.command(exp)
201+
await new Promise((resolve, reject) => {
202+
cmd.parse(`export --provider=@scope/l10n-service-provider \
203+
--output=./test/fixtures/locales \
204+
--normalize=hierarchy`, (err, argv, output) => {
205+
err ? reject(err) : resolve(output)
206+
})
207+
})
208+
209+
expect(mockExport).toHaveBeenCalledWith({
210+
locales: [],
211+
format: 'json',
212+
dryRun: false
213+
})
214+
})
215+
216+
test('--format option', async () => {
217+
// setup mocks
218+
const data = [
219+
{ locale: 'ja', data: Buffer.from(JSON.stringify({})) },
220+
{ locale: 'en', data: Buffer.from(JSON.stringify({})) }
221+
]
222+
mockExport.mockImplementation(({ locales, format }) => Promise.resolve(data))
223+
224+
// run
225+
const exp = await import('../../src/commands/export')
226+
const cmd = yargs.command(exp)
227+
await new Promise((resolve, reject) => {
228+
cmd.parse(`export --provider=@scope/l10n-service-provider \
229+
--output=./test/fixtures/locales \
230+
--format=xliff`, (err, argv, output) => {
231+
err ? reject(err) : resolve(output)
232+
})
233+
})
234+
235+
expect(mockExport).toHaveBeenCalledWith({
236+
locales: [],
237+
format: 'xliff',
238+
dryRun: false,
239+
normalize: undefined
240+
})
241+
})

0 commit comments

Comments
 (0)