Skip to content

Commit adb69fa

Browse files
committed
⭐ new: locale messages split and match options
closes #11
1 parent b7e8bbf commit adb69fa

File tree

7 files changed

+290
-11
lines changed

7 files changed

+290
-11
lines changed

src/commands/infuse.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { resolve, parsePath, readSFC } from '../utils'
44
import infuse from '../infuser'
55
import squeeze from '../squeezer'
66
import fs from 'fs'
7+
import path from 'path'
78
import { applyDiff } from 'deep-diff'
9+
import glob from 'glob'
810
import { LocaleMessages, SFCFileInfo, MetaLocaleMessage, Locale } from '../../types'
911

1012
import { debug as Debug } from 'debug'
@@ -13,6 +15,7 @@ const debug = Debug('vue-i18n-locale-message:commands:infuse')
1315
type InfuseOptions = {
1416
target: string
1517
messages: string
18+
match?: string
1619
}
1720

1821
export const command = 'infuse'
@@ -33,23 +36,44 @@ export const builder = (args: Argv): Argv<InfuseOptions> => {
3336
describe: 'locale messages path to be infused',
3437
demandOption: true
3538
})
39+
.option('match', {
40+
type: 'string',
41+
alias: 'r',
42+
describe: 'option should be accepted a regex filenames, must be specified together --messages'
43+
})
3644
}
3745

3846
export const handler = (args: Arguments<InfuseOptions>): void => {
3947
const targetPath = resolve(args.target)
4048
const messagesPath = resolve(args.messages)
4149
const sources = readSFC(targetPath)
42-
const messages = readLocaleMessages(messagesPath)
50+
const messages = readLocaleMessages(messagesPath, args.match)
4351
const meta = squeeze(targetPath, sources)
4452
apply(messages, meta)
4553
const newSources = infuse(targetPath, sources, meta)
4654
writeSFC(newSources)
4755
}
4856

49-
function readLocaleMessages (path: string): LocaleMessages {
50-
// TODO: async implementation
51-
const data = fs.readFileSync(path, { encoding: 'utf8' })
52-
return JSON.parse(data) as LocaleMessages
57+
function readLocaleMessages (targetPath: string, matchRegex?: string): LocaleMessages {
58+
debug('readLocaleMessages', targetPath, matchRegex)
59+
if (!matchRegex) {
60+
const data = fs.readFileSync(targetPath, { encoding: 'utf8' })
61+
return JSON.parse(data) as LocaleMessages
62+
} else {
63+
const globPath = path.normalize(`${targetPath}/*.json`)
64+
const paths = glob.sync(globPath)
65+
return paths.reduce((messages, p) => {
66+
const re = new RegExp(matchRegex, 'ig')
67+
const filename = path.basename(p)
68+
const match = re.exec(filename)
69+
debug('regex match', match)
70+
if (match) {
71+
const data = fs.readFileSync(p, { encoding: 'utf8' })
72+
Object.assign(messages, { [match[1]]: JSON.parse(data) })
73+
}
74+
return messages
75+
}, {} as LocaleMessages)
76+
}
5377
}
5478

5579
function removeItem<T> (item: T, items: T[]): boolean {

src/commands/squeeze.ts

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const debug = Debug('vue-i18n-locale-message:commands:squeeze')
1010

1111
type SqueezeOptions = {
1212
target: string
13+
split?: boolean
1314
output: string
1415
}
1516

@@ -26,6 +27,12 @@ export const builder = (args: Argv): Argv<SqueezeOptions> => {
2627
describe: 'target path that single-file components is stored',
2728
demandOption: true
2829
})
30+
.option('split', {
31+
type: 'boolean',
32+
alias: 's',
33+
default: false,
34+
describe: 'split squeezed locale messages with locale'
35+
})
2936
.option('output', {
3037
type: 'string',
3138
alias: 'o',
@@ -38,7 +45,7 @@ export const handler = (args: Arguments<SqueezeOptions>): void => {
3845
const targetPath = resolve(args.target)
3946
const meta = squeeze(targetPath, readSFC(targetPath))
4047
const messages = generate(meta)
41-
writeLocaleMessages(resolve(args.output), messages)
48+
writeLocaleMessages(messages, args)
4249
}
4350

4451
function generate (meta: MetaLocaleMessage): LocaleMessages {
@@ -85,9 +92,35 @@ function generate (meta: MetaLocaleMessage): LocaleMessages {
8592
return messages
8693
}
8794

88-
function writeLocaleMessages (output: string, messages: LocaleMessages) {
95+
function writeLocaleMessages (messages: LocaleMessages, args: Arguments<SqueezeOptions>) {
8996
// TODO: async implementation
90-
fs.writeFileSync(output, JSON.stringify(messages, null, 2))
97+
const split = args.split
98+
const output = resolve(args.output)
99+
if (!split) {
100+
fs.writeFileSync(output, JSON.stringify(messages, null, 2))
101+
} else {
102+
splitLocaleMessages(output, messages)
103+
}
104+
}
105+
106+
function splitLocaleMessages (path: string, messages: LocaleMessages) {
107+
const locales: Locale[] = Object.keys(messages)
108+
const write = () => {
109+
locales.forEach(locale => {
110+
fs.writeFileSync(`${path}/${locale}.json`, JSON.stringify(messages[locale], null, 2))
111+
})
112+
}
113+
try {
114+
fs.mkdirSync(path)
115+
write()
116+
} catch (err) {
117+
if (err.code === 'EEXIST') {
118+
write()
119+
} else {
120+
console.error(err.message)
121+
throw err
122+
}
123+
}
91124
}
92125

93126
export default {

test/__snapshots__/cli.test.ts.snap

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ Options:
1010
--help Show help [boolean]
1111
--target, -t target path that single-file components is stored
1212
[string] [required]
13-
--messages, -m locale messages path to be infused [string] [required]"
13+
--messages, -m locale messages path to be infused [string] [required]
14+
--match, -r option should be accepted a regex filenames, must be specified
15+
together --messages [string]"
1416
`;
1517

1618
exports[`squeeze command output help 1`] = `
@@ -23,6 +25,8 @@ Options:
2325
--help Show help [boolean]
2426
--target, -t target path that single-file components is stored
2527
[string] [required]
28+
--split, -s split squeezed locale messages with locale
29+
[boolean] [default: false]
2630
--output, -o path to output squeezed locale messages
2731
[string] [default: \\"/path/to/project1/messages.json\\"]"
2832
`;

test/commands/__snapshots__/infuse.test.ts.snap

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,119 @@ exports[`absolute path: /path/to/project1/src/pages/Login.vue 1`] = `
103103
</i18n>
104104
105105
106+
<i18n locale=\\"en\\">
107+
{
108+
\\"id\\": \\"User ID\\",
109+
\\"password\\": \\"Password\\",
110+
\\"confirm\\": \\"Confirm Password\\",
111+
\\"button\\": \\"Login\\"
112+
}
113+
</i18n>"
114+
`;
115+
116+
exports[`match option: /path/to/project1/src/App.vue 1`] = `
117+
"
118+
<template>
119+
<p>template</p>
120+
</template>
121+
122+
<script>
123+
export default {}
124+
</script>
125+
126+
<i18n>
127+
{
128+
\\"en\\": {
129+
\\"title\\": \\"Application\\"
130+
},
131+
\\"ja\\": {
132+
\\"title\\": \\"アプリケーション\\"
133+
}
134+
}
135+
</i18n>
136+
"
137+
`;
138+
139+
exports[`match option: /path/to/project1/src/components/Modal.vue 1`] = `
140+
"
141+
<template>
142+
<p>template</p>
143+
</template>
144+
145+
<script>
146+
export default {}
147+
</script>
148+
149+
<i18n locale=\\"en\\">
150+
{
151+
\\"ok\\": \\"OK\\",
152+
\\"cancel\\": \\"Cancel\\"
153+
}
154+
</i18n>
155+
156+
<i18n locale=\\"ja\\">
157+
{
158+
\\"ok\\": \\"OK\\",
159+
\\"cancel\\": \\"キャンセル\\"
160+
}
161+
</i18n>
162+
"
163+
`;
164+
165+
exports[`match option: /path/to/project1/src/components/nest/RankingTable.vue 1`] = `
166+
"
167+
<template>
168+
<p>template</p>
169+
</template>
170+
171+
<script>
172+
export default {}
173+
</script>
174+
175+
<i18n locale=\\"en\\">
176+
{
177+
\\"headers\\": {
178+
\\"rank\\": \\"Rank\\",
179+
\\"name\\": \\"Name\\",
180+
\\"score\\": \\"Score\\"
181+
}
182+
}
183+
</i18n>
184+
185+
186+
<i18n locale=\\"ja\\">
187+
{
188+
\\"headers\\": {
189+
\\"rank\\": \\"ランク\\",
190+
\\"name\\": \\"名前\\",
191+
\\"score\\": \\"スコア\\"
192+
}
193+
}
194+
</i18n>"
195+
`;
196+
197+
exports[`match option: /path/to/project1/src/pages/Login.vue 1`] = `
198+
"
199+
<template>
200+
<p>template</p>
201+
</template>
202+
203+
<script>
204+
export default {}
205+
</script>
206+
207+
<i18n>
208+
{
209+
\\"ja\\": {
210+
\\"id\\": \\"ユーザーID\\",
211+
\\"passowrd\\": \\"パスワード\\",
212+
\\"confirm\\": \\"パスワードの確認入力\\",
213+
\\"button\\": \\"ログイン\\"
214+
}
215+
}
216+
</i18n>
217+
218+
106219
<i18n locale=\\"en\\">
107220
{
108221
\\"id\\": \\"User ID\\",

test/commands/__snapshots__/squeeze.test.ts.snap

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,47 @@ Object {
9191
}",
9292
}
9393
`;
94+
95+
exports[`split option 1`] = `
96+
Object {
97+
"/path/to/project1/locales/en.json": "{
98+
\\"App\\": {
99+
\\"title\\": \\"Application\\"
100+
},
101+
\\"components\\": {
102+
\\"Modal\\": {
103+
\\"ok\\": \\"OK\\",
104+
\\"cancel\\": \\"Cancel\\"
105+
},
106+
\\"nest\\": {
107+
\\"RankingTable\\": {
108+
\\"headers\\": {
109+
\\"rank\\": \\"Rank\\",
110+
\\"name\\": \\"Name\\",
111+
\\"score\\": \\"Score\\"
112+
}
113+
}
114+
}
115+
}
116+
}",
117+
"/path/to/project1/locales/ja.json": "{
118+
\\"App\\": {
119+
\\"title\\": \\"アプリケーション\\"
120+
},
121+
\\"components\\": {
122+
\\"Modal\\": {
123+
\\"ok\\": \\"OK\\",
124+
\\"cancel\\": \\"キャンセル\\"
125+
}
126+
},
127+
\\"pages\\": {
128+
\\"Login\\": {
129+
\\"id\\": \\"ユーザーID\\",
130+
\\"passowrd\\": \\"パスワード\\",
131+
\\"confirm\\": \\"パスワードの確認入力\\",
132+
\\"button\\": \\"ログイン\\"
133+
}
134+
}
135+
}",
136+
}
137+
`;

test/commands/infuse.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as yargs from 'yargs'
22
import jsonMetaInfo from '../fixtures/meta/json'
33
import json from '../fixtures/squeeze'
4+
import path from 'path'
45

56
// -------
67
// mocking
@@ -12,6 +13,10 @@ const SFC_FILES = [
1213
`${TARGET_PATH}/src/components/nest/RankingTable.vue`,
1314
`${TARGET_PATH}/src/pages/Login.vue`
1415
]
16+
const LOCALE_FILES = [
17+
`${TARGET_PATH}/src/locales/ja.json`,
18+
`${TARGET_PATH}/src/locales/en.json`
19+
]
1520
const MOCK_FILES = SFC_FILES.reduce((files, file) => {
1621
const meta = jsonMetaInfo.find(meta => meta.contentPath === file)
1722
return Object.assign(files, { [file]: meta.raw })
@@ -36,7 +41,13 @@ jest.mock('fs', () => ({
3641
import fs from 'fs'
3742

3843
// mock: glob
39-
jest.mock('glob', () => ({ sync: jest.fn(() => SFC_FILES) }))
44+
jest.mock('glob', () => ({ sync: jest.fn((pattern) => {
45+
if (`${TARGET_PATH}/src/locales/*.json` === pattern) {
46+
return LOCALE_FILES
47+
} else {
48+
return SFC_FILES
49+
}
50+
}) }))
4051

4152
// -------------------
4253
// setup/teadown hooks
@@ -119,3 +130,37 @@ test('relative path', async () => {
119130
expect(value).toMatchSnapshot(key)
120131
}
121132
})
133+
134+
test('match option', async () => {
135+
// setup mocks
136+
const mockUtils = utils as jest.Mocked<typeof utils>
137+
mockUtils.resolve
138+
.mockImplementationOnce(() => `${TARGET_PATH}/src`)
139+
.mockImplementationOnce((...paths) => `${TARGET_PATH}/${paths[0]}`)
140+
const writeFiles = {}
141+
const mockFS = fs as jest.Mocked<typeof fs>
142+
mockFS.readFileSync.mockImplementation(p => {
143+
if (MOCK_FILES[p as string]) {
144+
return MOCK_FILES[p as string]
145+
} else {
146+
return JSON.stringify(json[path.basename(p as string, '.json')])
147+
}
148+
})
149+
mockFS.writeFileSync.mockImplementation((path, data) => {
150+
writeFiles[path as string] = data.toString()
151+
})
152+
153+
// run
154+
const infuse = await import('../../src/commands/infuse')
155+
const cmd = yargs.command(infuse)
156+
const output = await new Promise(resolve => {
157+
cmd.parse(`infuse --target=./src --messages=./src/locales --match=^([\\w-]*)\\.json`, () => {
158+
resolve(writeFiles)
159+
})
160+
})
161+
162+
// check
163+
for (const [key, value] of Object.entries(output)) {
164+
expect(value).toMatchSnapshot(key)
165+
}
166+
})

0 commit comments

Comments
 (0)