@ansstory/hias 1.0.4 → 1.0.6

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,68 +1,95 @@
1
- const inquirer = require('inquirer')
2
- const chalk = require('chalk')
3
- const { program } = require('commander')
4
- var config = require('../config')
5
- var downloadFun = require('./download')
6
- const compileEjs = require('../utils/compile-ejs')
7
- const writeFile = require('../utils/write-file')
8
- const beginAction = async (project, args) => {
9
- // 命令行的执行逻辑代码
10
- // console.log(project);
11
- // console.log(args);
12
- const answer = await inquirer.prompt([
13
- {
14
- type: 'list',
15
- name: 'framwork',
16
- choices: config.framwork,
17
- message: 'Please select the template.',
18
- },
19
- ])
20
- // console.log(answer);
21
- // 下载代码模板
22
- downloadFun(config.foramworkUrl[answer.framwork], project)
23
- }
24
-
25
- async function addComponentAction(cpnName, temPath, fileType) {
26
- // 边界处理: 若为 .\xx\xx\ 则转换为 ./xx/xx/
27
- const dest = program.opts().dest.replace(/\\/g, '/') || 'src/components'
28
- // 边界处理: 若为 ./xx/xx 则转换为 ./xx/xx/
29
- const path = /[\\/]/.test(dest.at(-1)) ? dest : `${dest}/`
30
-
31
- const [folderName = 'index'] = dest.match(/[^\\/]+(?=\/?$)/)
32
- const name = cpnName === 'index' ? folderName : cpnName
33
-
34
- const result = await compileEjs(temPath, {
35
- name: name,
36
- lowername: name.toLowerCase(),
37
- })
38
-
39
- const filePath = `${path}${cpnName}.${fileType}`
40
-
41
- await writeFile(filePath, result)
42
- console.log(chalk.green.bold('Component creation was successful:'), chalk.blue.bold(`${cpnName}.${fileType}`), chalk.underline(filePath))
43
- }
44
-
45
- async function addVueComponentAction(cpnName) {
46
- await addComponentAction(cpnName, 'component.vue.ejs', 'vue')
47
- }
48
- async function addReactComponentAction(cpnName) {
49
- await addComponentAction(cpnName, 'component.jsx.ejs', 'jsx')
50
- }
51
- async function addReactTsComponentAction(cpnName) {
52
- await addComponentAction(cpnName, 'component.tsx.ejs', 'tsx')
53
- }
54
- async function addReduxStoreAction(cpnName) {
55
- await addComponentAction(cpnName, 'reduxStore.jsx.ejs', 'jsx')
56
- }
57
- async function addReduxTsStoreAction(cpnName) {
58
- await addComponentAction(cpnName, 'reduxTsStore.tsx.ejs', 'tsx')
59
- }
60
-
61
- module.exports = {
62
- beginAction,
63
- addVueComponentAction,
64
- addReactComponentAction,
65
- addReactTsComponentAction,
66
- addReduxStoreAction,
67
- addReduxTsStoreAction,
68
- }
1
+ // 命令行操作处理文件 - 处理各种命令的具体执行逻辑
2
+
3
+ const inquirer = require('inquirer')
4
+ const chalk = require('chalk')
5
+ const { program } = require('commander')
6
+ var config = require('../config')
7
+ var downloadFun = require('./download')
8
+ const compileEjs = require('../utils/compile-ejs')
9
+ const writeFile = require('../utils/write-file')
10
+ const i18n = require('../i18n')
11
+
12
+ // 创建项目的主处理函数
13
+ const beginAction = async (project, args) => {
14
+ // 获取翻译函数
15
+ const t = i18n.getT()
16
+
17
+ // 使用 inquirer 交互式命令行提示用户选择框架
18
+ const answer = await inquirer.prompt([
19
+ {
20
+ type: 'list',
21
+ name: 'framwork',
22
+ choices: config.framwork,
23
+ message: t('prompts.selectTemplate'),
24
+ },
25
+ ])
26
+
27
+ // 调用下载函数获取对应框架的模板
28
+ downloadFun(config.foramworkUrl[answer.framwork], project)
29
+ }
30
+
31
+ // 通用的组件/Store 添加处理函数
32
+ async function addComponentAction(cpnName, temPath, fileType) {
33
+ // 获取翻译函数
34
+ const t = i18n.getT()
35
+
36
+ // 边界处理:将反斜杠转换为正斜杠(处理 Windows 路径格式)
37
+ const dest = program.opts().dest.replace(/\\/g, '/') || 'src/components'
38
+
39
+ // 边界处理:确保路径末尾带有斜杠
40
+ const path = /[\\/]/.test(dest.at(-1)) ? dest : `${dest}/`
41
+
42
+ // 从目标路径中提取文件夹名称(用作默认组件名)
43
+ const [folderName = 'index'] = dest.match(/[^\\/]+(?=\/?$)/)
44
+ // 如果组件名为 'index',使用文件夹名作为实际组件名
45
+ const name = cpnName === 'index' ? folderName : cpnName
46
+
47
+ // 使用 EJS 模板引擎编译模板文件
48
+ const result = await compileEjs(temPath, {
49
+ name: name,
50
+ lowername: name.toLowerCase(),
51
+ })
52
+
53
+ // 拼接完整的文件路径
54
+ const filePath = `${path}${cpnName}.${fileType}`
55
+
56
+ // 将编译后的内容写入文件
57
+ await writeFile(filePath, result)
58
+
59
+ // 输出成功提示信息
60
+ console.log(chalk.green.bold(t('messages.componentCreated')), chalk.blue.bold(`${cpnName}.${fileType}`), chalk.underline(filePath))
61
+ }
62
+
63
+ // 添加 Vue 组件
64
+ async function addVueComponentAction(cpnName) {
65
+ await addComponentAction(cpnName, 'component.vue.ejs', 'vue')
66
+ }
67
+
68
+ // 添加 React JSX 组件
69
+ async function addReactComponentAction(cpnName) {
70
+ await addComponentAction(cpnName, 'component.jsx.ejs', 'jsx')
71
+ }
72
+
73
+ // 添加 React TSX 组件
74
+ async function addReactTsComponentAction(cpnName) {
75
+ await addComponentAction(cpnName, 'component.tsx.ejs', 'tsx')
76
+ }
77
+
78
+ // 添加 Redux JSX Store
79
+ async function addReduxStoreAction(cpnName) {
80
+ await addComponentAction(cpnName, 'reduxStore.jsx.ejs', 'jsx')
81
+ }
82
+
83
+ // 添加 Redux TSX Store
84
+ async function addReduxTsStoreAction(cpnName) {
85
+ await addComponentAction(cpnName, 'reduxTsStore.tsx.ejs', 'tsx')
86
+ }
87
+
88
+ module.exports = {
89
+ beginAction,
90
+ addVueComponentAction,
91
+ addReactComponentAction,
92
+ addReactTsComponentAction,
93
+ addReduxStoreAction,
94
+ addReduxTsStoreAction,
95
+ }
@@ -0,0 +1,173 @@
1
+ // 关闭端口命令处理文件 - 查找并结束占用指定端口的进程
2
+
3
+ const { exec: execWithCallback } = require('child_process')
4
+ const { promisify } = require('util')
5
+
6
+ // 将 Node 回调风格的 exec 转换为 Promise,方便 async/await 调用
7
+ const defaultExec = promisify(execWithCallback)
8
+
9
+ // 端口参数标准化:校验端口必须是 1-65535 之间的数字
10
+ function normalizePort(port) {
11
+ const value = String(port || '').trim()
12
+
13
+ if (!/^\d+$/.test(value)) {
14
+ throw new Error('Invalid port. Port must be a number between 1 and 65535.')
15
+ }
16
+
17
+ const number = Number(value)
18
+ if (number < 1 || number > 65535) {
19
+ throw new Error('Invalid port. Port must be a number between 1 and 65535.')
20
+ }
21
+
22
+ return number
23
+ }
24
+
25
+ // 解析命令行端口输入,支持空格分隔和逗号分隔两种方式
26
+ function parsePortInputs(inputs) {
27
+ return unique(
28
+ inputs
29
+ .flatMap((input) => String(input).split(','))
30
+ .map((port) => port.trim())
31
+ .filter(Boolean)
32
+ .map(normalizePort),
33
+ )
34
+ }
35
+
36
+ // 从 netstat 的本地地址字段中提取端口号
37
+ function getLocalPort(localAddress) {
38
+ const match = String(localAddress).match(/:(\d+)$/)
39
+ return match ? Number(match[1]) : null
40
+ }
41
+
42
+ // 数组去重,保持原有顺序
43
+ function unique(values) {
44
+ return Array.from(new Set(values))
45
+ }
46
+
47
+ // 解析 Windows netstat 输出,提取占用目标端口的进程 PID
48
+ function parsePidsFromWindowsNetstat(output, port) {
49
+ return unique(
50
+ String(output)
51
+ .split(/\r?\n/)
52
+ .map((line) => line.trim())
53
+ .filter(Boolean)
54
+ .map((line) => line.split(/\s+/))
55
+ .filter((parts) => {
56
+ const protocol = parts[0]
57
+ const localAddress = parts[1]
58
+ const state = parts[parts.length - 2]
59
+
60
+ // 只处理本地端口匹配的连接
61
+ if (getLocalPort(localAddress) !== port) {
62
+ return false
63
+ }
64
+
65
+ // TCP 只结束监听进程,UDP 没有 LISTENING 状态,直接按端口匹配
66
+ return protocol === 'UDP' || (protocol === 'TCP' && state === 'LISTENING')
67
+ })
68
+ .map((parts) => parts[parts.length - 1])
69
+ .filter((pid) => /^\d+$/.test(pid)),
70
+ )
71
+ }
72
+
73
+ // 解析类 Unix 系统 lsof 输出,提取进程 PID
74
+ function parsePidsFromPosixLsof(output) {
75
+ return unique(
76
+ String(output)
77
+ .split(/\r?\n/)
78
+ .map((line) => line.trim())
79
+ .filter((line) => /^\d+$/.test(line)),
80
+ )
81
+ }
82
+
83
+ // 根据当前系统查找占用端口的进程 PID
84
+ async function findPortPids(port, options) {
85
+ const exec = options.exec || defaultExec
86
+ const platform = options.platform || process.platform
87
+
88
+ // Windows 使用 netstat 查询所有端口,再在 Node 内部解析过滤
89
+ if (platform === 'win32') {
90
+ const { stdout } = await exec('netstat -ano')
91
+ return parsePidsFromWindowsNetstat(stdout, port)
92
+ }
93
+
94
+ // macOS/Linux 使用 lsof 查询端口;如果系统未安装 lsof 或无占用,则视为无进程
95
+ try {
96
+ const { stdout } = await exec(`lsof -ti :${port}`)
97
+ return parsePidsFromPosixLsof(stdout)
98
+ } catch (err) {
99
+ return []
100
+ }
101
+ }
102
+
103
+ // 根据平台结束指定 PID
104
+ async function killPid(pid, options) {
105
+ const exec = options.exec || defaultExec
106
+ const platform = options.platform || process.platform
107
+ const command = platform === 'win32' ? `taskkill /F /PID ${pid}` : `kill -9 ${pid}`
108
+
109
+ await exec(command)
110
+ return pid
111
+ }
112
+
113
+ // 关闭单个端口:查找占用进程并逐个结束
114
+ async function closePort(port, options = {}) {
115
+ const normalizedPort = normalizePort(port)
116
+ const pids = await findPortPids(normalizedPort, options)
117
+ const killed = []
118
+
119
+ for (const pid of pids) {
120
+ killed.push(await killPid(pid, options))
121
+ }
122
+
123
+ return {
124
+ port: normalizedPort,
125
+ pids,
126
+ killed,
127
+ }
128
+ }
129
+
130
+ // 关闭多个端口:先解析输入,再按端口顺序逐个处理
131
+ async function closePorts(portInputs, options = {}) {
132
+ const ports = parsePortInputs(portInputs)
133
+ const results = []
134
+
135
+ for (const port of ports) {
136
+ results.push(await closePort(port, options))
137
+ }
138
+
139
+ return results
140
+ }
141
+
142
+ // CLI action:执行关闭端口命令并输出结果
143
+ async function handleClosePortAction(ports) {
144
+ const chalk = require('chalk')
145
+ const i18n = require('../i18n')
146
+ const t = i18n.getT()
147
+
148
+ try {
149
+ const results = await closePorts(ports)
150
+
151
+ // 每个端口单独输出处理结果,方便用户定位具体端口状态
152
+ for (const result of results) {
153
+ if (result.killed.length === 0) {
154
+ console.log(chalk.yellow.bold(t('messages.closePortNoProcess', { port: result.port })))
155
+ continue
156
+ }
157
+
158
+ console.log(chalk.green.bold(t('messages.closePortSuccess', { port: result.port, pids: result.killed.join(', ') })))
159
+ }
160
+ } catch (err) {
161
+ console.log(chalk.red.bold(err.message))
162
+ }
163
+ }
164
+
165
+ module.exports = {
166
+ closePort,
167
+ closePorts,
168
+ handleClosePortAction,
169
+ normalizePort,
170
+ parsePortInputs,
171
+ parsePidsFromPosixLsof,
172
+ parsePidsFromWindowsNetstat,
173
+ }
@@ -1,40 +1,106 @@
1
- const chalk = require('chalk')
2
- const {
3
- beginAction,
4
- addVueComponentAction,
5
- addReactComponentAction,
6
- addReactTsComponentAction,
7
- addReduxStoreAction,
8
- addReduxTsStoreAction,
9
- } = require('./action')
10
-
11
- const beginCommander = function (program) {
12
- console.log()
13
- program.command('create <project> [other...]').alias('crt').description('create project. For example: hias create airbnb').action(beginAction)
14
- program
15
- .command('adv <vuecpnname> [...others]')
16
- .description(`add vue component into a folder, For example: ${chalk.green.bold(`hias addv Demo -d src/components or hias addv index -d src/views/demo`)}`)
17
- .action(addVueComponentAction)
18
- program
19
- .command('adr <reactcpnname> [...others]')
20
- .description(
21
- `add react jsx component into a folder, For example: ${chalk.green.bold(`hias addr Demo -d src/components or hias addr index -d src/views/demo`)}`
22
- )
23
- .action(addReactComponentAction)
24
- program
25
- .command('adrt <reactcpnname> [...others]')
26
- .description(
27
- `add react tsx component into a folder, For example: ${chalk.green.bold(`hias addrt Demo -d src/components or hias addrt index -d src/views/demo`)}`
28
- )
29
- .action(addReactTsComponentAction)
30
- program
31
- .command('adrd <reactcpnname> [...others]')
32
- .description(`add redux jsx store into a folder, For example: ${chalk.blue.bold(`hias adrd useDemoStore -d src/store/modules`)}`)
33
- .action(addReduxStoreAction)
34
- program
35
- .command('adrdt <reactcpnname> [...others]')
36
- .description(`add redux tsx store into a folder, For example: ${chalk.blue.bold(`hias adrdt useDemoStore -d src/store/modules`)}`)
37
- .action(addReduxTsStoreAction)
38
- }
39
-
40
- module.exports = beginCommander
1
+ // 命令行指令定义文件 - 配置 CLI 的所有可用命令
2
+
3
+ const chalk = require('chalk')
4
+ const i18n = require('../i18n')
5
+ const {
6
+ beginAction,
7
+ addVueComponentAction,
8
+ addReactComponentAction,
9
+ addReactTsComponentAction,
10
+ addReduxStoreAction,
11
+ addReduxTsStoreAction,
12
+ } = require('./action')
13
+ const { handleLanguageAction } = require('./lang')
14
+ const { handleClosePortAction } = require('./close-port')
15
+ const { handleTranslateFileAction, handleTranslateFolderAction, handleSettingAction, handleGlobalSettingAction, handleRollbackAction, handleGitignoreAction } = require('./translate')
16
+
17
+ // 注册所有命令行指令
18
+ const beginCommander = function (program) {
19
+ // 获取翻译函数
20
+ const t = i18n.getT()
21
+
22
+ console.log()
23
+
24
+ // 创建项目命令:下载框架模板
25
+ program.command('create <project> [other...]').alias('crt').description(t('commands.create')).action(beginAction)
26
+
27
+ // 添加 Vue 组件命令
28
+ program.command('adv <vuecpnname> [...others]').description(t('commands.adv')).action(addVueComponentAction)
29
+
30
+ // 添加 React JSX 组件命令
31
+ program.command('adr <reactcpnname> [...others]').description(t('commands.adr')).action(addReactComponentAction)
32
+
33
+ // 添加 React TSX 组件命令
34
+ program.command('adrt <reactcpnname> [...others]').description(t('commands.adrt')).action(addReactTsComponentAction)
35
+
36
+ // 添加 Redux JSX Store 命令
37
+ program.command('adrd <reactcpnname> [...others]').description(t('commands.adrd')).action(addReduxStoreAction)
38
+
39
+ // 添加 Redux TSX Store 命令
40
+ program.command('adrdt <reactcpnname> [...others]').description(t('commands.adrdt')).action(addReduxTsStoreAction)
41
+
42
+ // 语言切换命令
43
+ program.command('lang [language]').description(t('commands.lang')).option('-l --list', 'show current language').action(handleLanguageAction)
44
+
45
+ // 关闭端口命令
46
+ program.command('close-port <ports...>').description(t('commands.closePort')).action(handleClosePortAction)
47
+
48
+ // 翻译文件命令:将单个文件中的中文替换为国际化调用并生成语言包
49
+ program
50
+ .command('tf [file] [name]')
51
+ .description(t('commands.translateFile'))
52
+ .option('-n, --name <name>', t('options.translateName'))
53
+ .option('--dry-run', 'preview changes without modifying files')
54
+ .option('--show-extractions', 'list all extracted Chinese texts without translating')
55
+ .option('-v, --verbose', 'verbose output with individual file logs')
56
+ .action((file, name, options) => {
57
+ name = name || options.name
58
+ options.name = name
59
+ if (!file) {
60
+ console.log(chalk.red(t('messages.translateFileRequired')))
61
+ return
62
+ }
63
+ handleTranslateFileAction(file, options)
64
+ })
65
+
66
+ // 翻译文件夹命令:递归翻译文件夹内所有支持的文件
67
+ program
68
+ .command('tfo [folder] [name]')
69
+ .description(t('commands.translateFolder'))
70
+ .option('-n, --name <name>', t('options.translateName'))
71
+ .option('--dry-run', 'preview changes without modifying files')
72
+ .option('--show-extractions', 'list all extracted Chinese texts without translating')
73
+ .option('--exclude <pattern>', 'exclude files matching glob pattern (e.g. **/*.test.js)', (val, acc) => acc.concat([val]), [])
74
+ .option('-v, --verbose', 'verbose output with individual file logs')
75
+ .action((folder, name, options) => {
76
+ name = name || options.name
77
+ options.name = name
78
+ if (!folder) {
79
+ console.log(chalk.red(t('messages.translateFolderRequired')))
80
+ return
81
+ }
82
+ handleTranslateFolderAction(folder, options)
83
+ })
84
+
85
+ // 生成翻译配置文件(项目级)
86
+ program.command('setting').description(t('commands.setting')).action(handleSettingAction)
87
+
88
+ // 生成全局翻译配置文件
89
+ program.command('globalsetting').description(t('commands.globalSetting')).action(handleGlobalSettingAction)
90
+
91
+ // 将 .hias 添加到 .gitignore
92
+ program.command('gitignore').description(t('commands.gitignore')).action(handleGitignoreAction)
93
+
94
+ // 回滚上一次翻译操作
95
+ program
96
+ .command('rollback')
97
+ .description(t('commands.rollback'))
98
+ .option('-l, --list', 'list available backups')
99
+ .option('--name <name>', 'specific backup name to rollback (default: latest)')
100
+ .option('--keep <count>', 'keep only N most recent backups, remove older ones')
101
+ .action((options) => {
102
+ handleRollbackAction(options)
103
+ })
104
+ }
105
+
106
+ module.exports = beginCommander
@@ -1,19 +1,32 @@
1
- const download = require('download-git-repo')
2
- const ora = require('ora')
3
- const chalk = require('chalk')
4
- const downloadFun = function (url, project) {
5
- const spinner = ora().start()
6
- spinner.text = 'in progress...'
7
- download('direct:' + url, project, { clone: true }, (err) => {
8
- if (!err) {
9
- spinner.succeed('succeed')
10
- console.log(chalk.blue.bold('you run:'))
11
- console.log(chalk.blue.bold('cd ' + project))
12
- console.log(chalk.blue.bold('code .'))
13
- } else {
14
- spinner.fail(`error: ${err}`)
15
- }
16
- })
17
- }
18
-
19
- module.exports = downloadFun
1
+ // 项目模板下载处理文件 - 从 Git 仓库下载框架模板
2
+
3
+ const download = require('download-git-repo')
4
+ const ora = require('ora')
5
+ const chalk = require('chalk')
6
+ const i18n = require('../i18n')
7
+
8
+ // 下载框架模板函数
9
+ const downloadFun = function (url, project) {
10
+ // 获取翻译函数
11
+ const t = i18n.getT()
12
+
13
+ // 创建加载动画
14
+ const spinner = ora().start()
15
+ spinner.text = t('messages.downloading')
16
+
17
+ // 使用 download-git-repo 库下载 Git 仓库
18
+ download('direct:' + url, project, { clone: true }, (err) => {
19
+ if (!err) {
20
+ // 下载成功,停止加载动画并显示成功提示
21
+ spinner.succeed(t('messages.downloadSuccess'))
22
+ console.log(chalk.blue.bold(t('messages.youRun')))
23
+ console.log(chalk.blue.bold('cd ' + project))
24
+ console.log(chalk.blue.bold('code .'))
25
+ } else {
26
+ // 下载失败,显示错误信息
27
+ spinner.fail(`${t('messages.downloadError')}: ${err}`)
28
+ }
29
+ })
30
+ }
31
+
32
+ module.exports = downloadFun
package/lib/core/help.js CHANGED
@@ -1,6 +1,17 @@
1
- const beginHelp = function (program) {
2
- const version = require('../../package.json').version
3
- program.version(version, '-v --version')
4
- program.option('-d --dest <dest>', 'a destination folder, For example: -d src/components')
5
- }
6
- module.exports = beginHelp
1
+ // 帮助信息配置 - 定义 CLI 版本号和全局选项
2
+
3
+ const i18n = require('../i18n')
4
+
5
+ const beginHelp = function (program) {
6
+ // 获取翻译函数
7
+ const t = i18n.getT()
8
+
9
+ // 从 package.json 中读取版本号
10
+ const version = require('../../package.json').version
11
+ // 设置版本号命令
12
+ program.version(version, '-v --version')
13
+ // 定义全局目标目录选项
14
+ program.option('-d --dest <dest>', t('options.dest'))
15
+ }
16
+
17
+ module.exports = beginHelp
@@ -0,0 +1,50 @@
1
+ // 语言切换命令处理 - 处理用户设置 CLI 语言的操作
2
+
3
+ const chalk = require('chalk')
4
+ const i18n = require('../i18n')
5
+
6
+ /**
7
+ * 处理语言切换命令
8
+ * @param {string} language - 语言代码
9
+ * @param {object} options - 命令选项
10
+ */
11
+ const handleLanguageAction = (language, options) => {
12
+ const t = i18n.getT()
13
+
14
+ // 查看当前语言
15
+ if (options.list || options.l) {
16
+ const currentLang = i18n.getCurrentLanguage()
17
+ const langName = t(`languages.${currentLang}`)
18
+ console.log(chalk.blue.bold(t('messages.langCurrent', { language: langName })))
19
+ return
20
+ }
21
+
22
+ // 如果没有提供语言参数
23
+ if (!language) {
24
+ console.log(chalk.yellow.bold('Usage: hias lang <language> | hias lang -l'))
25
+ console.log(chalk.gray(`Supported languages: ${i18n.getSupportedLanguages().join(', ')}`))
26
+ return
27
+ }
28
+
29
+ // 验证语言是否支持
30
+ if (!i18n.SUPPORTED_LANGUAGES.includes(language)) {
31
+ const supportedLangs = i18n
32
+ .getSupportedLanguages()
33
+ .map((lang) => `${lang} (${t(`languages.${lang}`)})`)
34
+ .join(', ')
35
+ console.log(chalk.red.bold(t('messages.invalidLanguage', { languages: supportedLangs })))
36
+ return
37
+ }
38
+
39
+ // 设置语言
40
+ if (i18n.changeLanguage(language)) {
41
+ const langName = t(`languages.${language}`)
42
+ console.log(chalk.green.bold(t('messages.langSet', { language: langName })))
43
+ } else {
44
+ console.log(chalk.red.bold('Failed to set language'))
45
+ }
46
+ }
47
+
48
+ module.exports = {
49
+ handleLanguageAction,
50
+ }