Skip to content

Commit bef5786

Browse files
authored
feat: i18n (#3)
1 parent 7e0bd86 commit bef5786

File tree

7 files changed

+204
-26
lines changed

7 files changed

+204
-26
lines changed

README.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
CLI Tooling for i18n development
44

5-
> WIP :construction:
6-
75
## :cd: Installation
86

97
### npm
@@ -20,13 +18,28 @@ yarn global @intlify/cli
2018

2119
## :rocket: Usage
2220

23-
TODO:
21+
```
22+
Usage: intlify <command> [options]
23+
24+
Commands:
25+
intlify compile compile the i18n resources [aliases: cp]
26+
27+
Options:
28+
--help Show help [boolean]
29+
--version Show version number [boolean]
30+
```
2431

2532
## :handshake: API
2633

2734
About details, See the [API References](https://github.com/intlify/cli/blob/master/api.md)
2835

2936

37+
## :globe_with_meridians: I18n
38+
39+
Intlify cli is supporting for I18n.
40+
41+
If you would like to localiize Intlify CLI, you can contribute i18n resource to [locales](https://github.com/intlify/cli/blob/master/locales) directory.
42+
3043
## :scroll: Changelog
3144
Details changes for each release are documented in the [CHANGELOG.md](https://github.com/intlify/cli/blob/master/CHANGELOG.md).
3245

locales/en.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"Usage: $0 <command> [options]": "Usage: $0 <command> [options]",
3+
"compile the i18n resources": "compile the i18n resources",
4+
"the i18n resource source path": "the i18n resource source path",
5+
"the compiled i18n resource output path": "the compiled i18n resource output path",
6+
"Success compilation: {source} -> {output}": "Success compilation: {source} -> {output}",
7+
"{source}: Ignore compilation due to not supported '{ext}'": "{source}: Ignore compilation due to not supported '{ext}'",
8+
"Warning compilation: {source} -> {output}, {msg}": "Warning compilation: {source} -> {output}, {msg}",
9+
"Error compilation: {source} -> {output}, {msg}": "Error compilation: {source} -> {output}, {msg}"
10+
}

locales/ja.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"Usage: $0 <command> [options]": "使い方: $0 <command> [options]",
3+
"compile the i18n resources": "i18nリソースのコンパイル",
4+
"the i18n resource source path": "i18n リソースのソースパス",
5+
"the compiled i18n resource output path": "コンパイルされた i18n リソースの出力先パス",
6+
"Success compilation: {source} -> {output}": "コンパイル成功: {source} -> {output}",
7+
"{source}: Ignore compilation due to not supported '{ext}'": "{source}: '{ext}' がサポートされていないためコンパルを無視します",
8+
"Warning compilation: {source} -> {output}, {msg}": "コンパイル警告: {source} -> {output}, {msg}",
9+
"Error compilation: {source} -> {output}, {msg}": "コンパイルエラー: {source} -> {output}, {msg}"
10+
}

src/cli.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import * as yargs from 'yargs'
2+
import { initI18n, t } from './i18n'
3+
;(async () => {
4+
await initI18n()
5+
yargs
6+
.usage(t('Usage: $0 <command> [options]'))
7+
.commandDir('./commands')
8+
.demandCommand()
9+
.help()
10+
.version().argv
211

3-
yargs
4-
.usage('Usage: $0 <command> [options]')
5-
.commandDir('./commands')
6-
.demandCommand()
7-
.help()
8-
.version().argv
12+
process.on('uncaughtException', err => {
13+
console.error(`uncaught exception: ${err}\n`)
14+
process.exit(1)
15+
})
916

10-
process.on('uncaughtException', err => {
11-
console.error(`uncaught exception: ${err}\n`)
12-
process.exit(1)
13-
})
14-
15-
process.on('unhandledRejection', (reason, p) => {
16-
console.error('unhandled rejection at:', p, 'reason:', reason)
17-
process.exit(1)
18-
})
17+
process.on('unhandledRejection', (reason, p) => {
18+
console.error('unhandled rejection at:', p, 'reason:', reason)
19+
process.exit(1)
20+
})
21+
})()

src/commands/compile.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import path from 'path'
22
import chalk from 'chalk'
3-
import { Arguments, Argv } from 'yargs'
43
import { debug as Debug } from 'debug'
54
import { CompileErrorCodes, compile } from '../api'
5+
import { t } from '../i18n'
6+
7+
import type { Arguments, Argv } from 'yargs'
68

79
const debug = Debug('@intlify/cli:compile')
810

@@ -13,20 +15,20 @@ type CompileOptions = {
1315

1416
export const command = 'compile'
1517
export const aliases = 'cp'
16-
export const describe = 'compile the i18n resources'
18+
export const describe = t('compile the i18n resources')
1719

1820
export const builder = (args: Argv): Argv<CompileOptions> => {
1921
return args
2022
.option('source', {
2123
type: 'string',
2224
alias: 's',
23-
describe: 'the source i18n resource path',
25+
describe: t('the i18n resource source path'),
2426
demandOption: true
2527
})
2628
.option('output', {
2729
type: 'string',
2830
alias: 'o',
29-
describe: 'the compiled i18n resource output path'
31+
describe: t('the compiled i18n resource output path')
3032
})
3133
}
3234

@@ -37,7 +39,13 @@ export const handler = async (
3739
args.output != null ? path.resolve(__dirname, args.output) : process.cwd()
3840
const ret = await compile(args.source, output, {
3941
onCompile: (source: string, output: string): void => {
40-
console.log(chalk.green(`success compilation: ${source} -> ${output}`))
42+
console.log(
43+
chalk.green(
44+
t('Success compilation: {source} -> {output}', {
45+
named: { source, output }
46+
})
47+
)
48+
)
4149
},
4250
onError: (
4351
code: number,
@@ -50,18 +58,28 @@ export const handler = async (
5058
const parsed = path.parse(source)
5159
console.warn(
5260
chalk.yellow(
53-
`${source}: Ignore compilation due to not supported '${parsed.ext}'`
61+
t("{source}: Ignore compilation due to not supported '{ext}'", {
62+
named: { ext: parsed.ext }
63+
})
5464
)
5565
)
5666
break
5767
case CompileErrorCodes.INTERNAL_COMPILE_WARNING:
5868
console.log(
59-
chalk.yellow(`warning compilation: ${source} -> ${output}, ${msg}`)
69+
chalk.yellow(
70+
t('Warning compilation: {source} -> {output}, {msg}', {
71+
named: { source, output, msg }
72+
})
73+
)
6074
)
6175
break
6276
case CompileErrorCodes.INTERNAL_COMPILE_ERROR:
6377
console.log(
64-
chalk.green(`error compilation: ${source} -> ${output}, ${msg}`)
78+
chalk.green(
79+
t('Error compilation: {source} -> {output}, {msg}', {
80+
named: { source, output, msg }
81+
})
82+
)
6583
)
6684
break
6785
default:

src/i18n.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import path from 'path'
2+
import { promises as fs } from 'fs'
3+
import { isString } from '@intlify/shared'
4+
import { createCoreContext, translate } from '@intlify/core'
5+
import { debug as Debug } from 'debug'
6+
import i18nResourceSchema from '../locales/en.json'
7+
8+
import type {
9+
Locale,
10+
LocaleMessages,
11+
LocaleMessageDictionary,
12+
TranslateOptions,
13+
CoreContext
14+
} from '@intlify/core'
15+
16+
const debug = Debug('@intlify/cli:i18n')
17+
const DEFAULT_LOCALE = 'en-US'
18+
19+
let resources: LocaleMessages | null = null
20+
let context: CoreContext<unknown, unknown, unknown, string> | null = null
21+
22+
async function loadI18nResources(): Promise<LocaleMessages> {
23+
const dirents = await fs.readdir(path.resolve(__dirname, '../../locales'), {
24+
withFileTypes: true
25+
})
26+
return dirents.reduce(async (acc, dir) => {
27+
if (dir.isFile()) {
28+
const data = await fs.readFile(
29+
path.resolve(__dirname, '../../locales', dir.name),
30+
{ encoding: 'utf-8' }
31+
)
32+
const { name } = path.parse(dir.name)
33+
debug('load i18n resource', name, data)
34+
const messages = await acc
35+
messages[name] = JSON.parse(data) as LocaleMessageDictionary
36+
}
37+
return acc
38+
}, Promise.resolve({} as LocaleMessages))
39+
}
40+
41+
export function getLocale(env?: Record<string, unknown>): Locale {
42+
env = env || process.env
43+
const raw =
44+
((env.LC_ALL || env.LC_MESSAGES || env.LANG || env.LANGUAGE) as string) ||
45+
DEFAULT_LOCALE
46+
debug('getLocale', raw)
47+
return raw.replace(/\.UTF\-8/g, '').replace(/_/g, '-')
48+
}
49+
50+
export function t<Key extends keyof typeof i18nResourceSchema>(
51+
key: Key,
52+
options?: TranslateOptions
53+
): string {
54+
if (context == null) {
55+
console.error(
56+
'cannot initialize CoreContext with @intlify/core createCoreContext'
57+
)
58+
return key
59+
}
60+
const ret = translate(context, key, options)
61+
return isString(ret) ? ret : key
62+
}
63+
64+
export async function initI18n() {
65+
try {
66+
resources = await loadI18nResources()
67+
context = createCoreContext({
68+
locale: getLocale(),
69+
fallbackLocale: DEFAULT_LOCALE,
70+
fallbackWarn: false,
71+
missingWarn: false,
72+
warnHtmlMessage: false,
73+
fallbackFormat: true,
74+
messages: resources
75+
})
76+
} catch (e) {
77+
debug('load i18n resource errors', e.message)
78+
throw e
79+
}
80+
}

test/i18n.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { getLocale } from '../src/i18n'
2+
3+
let ORG_ENV = {}
4+
5+
beforeEach(() => {
6+
jest.resetModules()
7+
ORG_ENV = process.env
8+
process.env = {}
9+
})
10+
11+
afterEach(() => {
12+
process.env = ORG_ENV
13+
})
14+
15+
describe('getLocale', () => {
16+
test('default', () => {
17+
expect(getLocale()).toEqual('en-US')
18+
})
19+
20+
test('LC_ALL', () => {
21+
process.env['LC_ALL'] = 'ja_JP'
22+
expect(getLocale()).toEqual('ja-JP')
23+
})
24+
25+
test('LC_MESSAGES', () => {
26+
process.env['LC_MESSAGES'] = 'ja_JP'
27+
expect(getLocale()).toEqual('ja-JP')
28+
})
29+
30+
test('LANG', () => {
31+
process.env['LANG'] = 'ja_JP'
32+
expect(getLocale()).toEqual('ja-JP')
33+
})
34+
35+
test('LANGUAGE', () => {
36+
process.env['LANGUAGE'] = 'ja_JP'
37+
expect(getLocale()).toEqual('ja-JP')
38+
})
39+
40+
test('BCP-47', () => {
41+
process.env['LC_ALL'] = 'ja-JP'
42+
expect(getLocale()).toEqual('ja-JP')
43+
})
44+
})

0 commit comments

Comments
 (0)