@picgo/i18n
Advanced tools
Comparing version 0.0.1 to 0.0.2
@@ -1,16 +0,10 @@ | ||
import { II18nConstructorOptions } from './types/index'; | ||
import { II18nConstructorOptions } from './types'; | ||
export declare class I18n { | ||
private language; | ||
private locales; | ||
private localeFileName; | ||
private readonly localesBaseDir; | ||
private readonly logger; | ||
private adapter; | ||
private currentLanguage; | ||
constructor(options: II18nConstructorOptions); | ||
getLanguage(): string; | ||
setLanguage(language: string): void; | ||
translate(phrase: string, args: any): any; | ||
translate(phrase: string, args: any): string | undefined; | ||
private postProcess; | ||
private loadLocale; | ||
private guessLocaleFileName; | ||
private watch; | ||
} |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const tslib_1 = require("tslib"); | ||
const fs = tslib_1.__importStar(require("fs")); | ||
const path = tslib_1.__importStar(require("path")); | ||
const index_1 = require("./types/index"); | ||
const logger_1 = require("./logger"); | ||
const utils_1 = require("./utils"); | ||
const DOTNOTATION = '.'; | ||
class I18n { | ||
constructor(options) { | ||
this.localeFileName = {}; // localeName -> the name of localeFile | ||
this.logger = new logger_1.Logger(); | ||
this.locales = {}; | ||
const { language, localesBaseDir, localeFileName } = options; | ||
this.localesBaseDir = localesBaseDir; | ||
if (localeFileName) { | ||
this.localeFileName = localeFileName; | ||
} | ||
else { | ||
this.guessLocaleFileName(localesBaseDir); | ||
} | ||
this.language = language; | ||
this.loadLocale(language); // load locale | ||
if (options.autoReoload) { | ||
this.watch(this.localesBaseDir); | ||
} | ||
const { adapter, defaultLanguage } = options; | ||
this.adapter = adapter; | ||
this.currentLanguage = defaultLanguage.trim().toLowerCase(); | ||
} | ||
getLanguage() { | ||
return this.language; | ||
return this.currentLanguage; | ||
} | ||
setLanguage(language) { | ||
this.language = language; | ||
if (!this.locales[language]) { | ||
this.loadLocale(language); | ||
} | ||
this.currentLanguage = language.trim().toLowerCase(); | ||
} | ||
translate(phrase, args) { | ||
const currentLocale = this.locales[this.language]; | ||
const currentLocale = this.adapter.getLocale(this.currentLanguage); | ||
if (!currentLocale) { | ||
this.logger.error('current locale is null'); | ||
utils_1.logger.error('current locale is null'); | ||
return; | ||
@@ -45,3 +25,3 @@ } | ||
if (!object || !object.hasOwnProperty(key)) { | ||
this.logger.warn(`current locale does\'t contain ${phrase}`); | ||
utils_1.logger.warn(`current locale does\'t contain ${phrase}`); | ||
return; | ||
@@ -57,55 +37,8 @@ } | ||
} | ||
const keys = Object.keys(args); | ||
const values = keys.map((key) => args[key]); | ||
return new Function(keys.join(','), `return \`${template}\``)(values); | ||
// see benchmark | ||
return Object.keys(args).reduce((res, key) => { | ||
return res.replace('${' + key + '}', args[key]); | ||
}, template); | ||
} | ||
loadLocale(language) { | ||
if (!this.localeFileName[language]) { | ||
this.logger.error(`can\'t locate the locale file of language ${language}`); | ||
return; | ||
} | ||
const filePath = path.join(this.localesBaseDir, this.localeFileName[language]); | ||
const fileContent = fs.readFileSync(filePath, { | ||
encoding: 'utf-8', | ||
}); | ||
try { | ||
const locale = JSON.parse(fileContent); | ||
this.locales[language] = locale; // load locale content | ||
} | ||
catch (err) { | ||
this.logger.error(`unable to parse locales from file (maybe ${filePath} is empty or invalid json?)`); | ||
this.logger.error(`raw error info: ${err}`); | ||
} | ||
} | ||
// TODO change the name of this method | ||
guessLocaleFileName(dir) { | ||
const files = fs.readdirSync(dir); | ||
const localeFileName = {}; | ||
files.forEach((fileName) => { | ||
const localeName = fileName.replace(path.extname(fileName), ''); | ||
localeFileName[localeName] = fileName; | ||
}); | ||
this.logger.log(`guess locale file path from ${dir}`); | ||
this.logger.log(`localeFileName: ${JSON.stringify(localeFileName)}`); | ||
this.localeFileName = localeFileName; | ||
} | ||
// useless ? | ||
watch(dir) { | ||
fs.watch(dir, (eventType, fileName) => { | ||
let language = ''; | ||
const { localeFileName } = this; | ||
for (const lan in localeFileName) { | ||
if (localeFileName.hasOwnProperty(lan)) { | ||
if (localeFileName[lan] === fileName) { | ||
language = lan; | ||
} | ||
} | ||
} | ||
if (language && eventType === index_1.EFileChangeType.change) { | ||
this.loadLocale(language); // update locale | ||
this.logger.log(`${fileName} has updated`); | ||
} | ||
}); | ||
} | ||
} | ||
exports.I18n = I18n; |
@@ -1,1 +0,3 @@ | ||
export { I18n } from "./i18n"; | ||
export * from './adapters'; | ||
export * from './utils'; | ||
export * from './i18n'; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
var i18n_1 = require("./i18n"); | ||
exports.I18n = i18n_1.I18n; | ||
const tslib_1 = require("tslib"); | ||
tslib_1.__exportStar(require("./adapters"), exports); | ||
tslib_1.__exportStar(require("./utils"), exports); | ||
tslib_1.__exportStar(require("./i18n"), exports); |
@@ -0,7 +1,16 @@ | ||
import { BaseAdapter } from '../adapters'; | ||
export interface ILocaleFileName { | ||
[locale: string]: string; | ||
} | ||
export interface ILocale { | ||
[key: string]: any; | ||
} | ||
export interface ILocaleMap { | ||
[language: string]: ILocale; | ||
} | ||
export interface II18nConstructorOptions { | ||
autoReoload?: boolean; | ||
language: string; | ||
adapter: BaseAdapter; | ||
defaultLanguage: string; | ||
} | ||
export interface IFileSyncAdapterConstructorOptions { | ||
localesBaseDir: string; | ||
@@ -13,1 +22,4 @@ localeFileName?: ILocaleFileName; | ||
} | ||
export declare enum ERUN_ENV { | ||
dev = "development" | ||
} |
@@ -7,1 +7,5 @@ "use strict"; | ||
})(EFileChangeType = exports.EFileChangeType || (exports.EFileChangeType = {})); | ||
var ERUN_ENV; | ||
(function (ERUN_ENV) { | ||
ERUN_ENV["dev"] = "development"; | ||
})(ERUN_ENV = exports.ERUN_ENV || (exports.ERUN_ENV = {})); |
{ | ||
"name": "@picgo/i18n", | ||
"version": "0.0.1", | ||
"version": "0.0.2", | ||
"description": "i18n tool", | ||
@@ -9,8 +9,8 @@ "main": "dist/index.js", | ||
"dev": "tsc -w -p .", | ||
"build": "rollup -c", | ||
"lint": "eslint src/**/*.ts", | ||
"codestyle:check": "prettier --check ./src/**/*.ts **/**.json", | ||
"codestyle:fix": "prettier --write ./src/**/*.ts **/**.json", | ||
"build": "tsc -p . && rollup -c", | ||
"lint": "eslint ./src{/,/**/}*.ts", | ||
"codestyle:check": "prettier --check ./src{/,/**/}*.ts **/**.json", | ||
"codestyle:fix": "prettier --write ./src{/,/**/}*.ts **/**.json", | ||
"test": "mocha ./test/index.js", | ||
"cz": "npm run codestyle:check && git-cz", | ||
"cz": "npm run codestyle:check && npm run lint && git-cz", | ||
"release": "bump-version" | ||
@@ -17,0 +17,0 @@ }, |
148
README.md
## i18n | ||
## Usage | ||
![CI Test](https://github.com/PicGo/i18n/workflows/CI%20Test/badge.svg) | ||
i18n 工具 | ||
### 用法 | ||
i18n 默认提供 FileSyncAdapter、ObjectAdapter 两个适配器,适用的场景分别为: locales 信息保存在文件中和 locales 信息保存在对象中。 | ||
```js | ||
import { I18n } from '@picgo/i18n'; | ||
import { I18n, FileSyncAdapter, ObjectAdapter } from '@picgo/i18n'; | ||
// use FileSyncAdapter | ||
const fileSyncAdapter = new FileSyncAdapter({ | ||
localesBaseDir: path.resolve(__dirname, './locales'), // locales文件目录 | ||
}); | ||
const i18n = new I18n({ | ||
autoReoload: true, | ||
language: DEFAULT_LANGUAGE, | ||
localesBaseDir: YOUR_LOCALES_DIR, | ||
adapter: fileSyncAdapter, | ||
defaultLanguage: 'zh', | ||
}); | ||
// use ObjectAdapter | ||
const objectAdapter = new ObjectAdapter({ | ||
zh: { | ||
user: { | ||
name: 'PicGo', | ||
country: '中国', | ||
}, | ||
report: { | ||
singular: ' ${cnt}个报告', | ||
plural: '${cnt}个报告', | ||
}, | ||
}, | ||
en: { | ||
user: { | ||
name: 'PicGo', | ||
country: 'China', | ||
}, | ||
report: { | ||
singular: 'only ${cnt} report', | ||
plural: '${cnt} reports', | ||
}, | ||
}, | ||
}); | ||
const i18n = new I18n({ | ||
adapter: objectAdapter, | ||
defaultLanguage: 'zh', | ||
}); | ||
``` | ||
### 自定义 Adapter | ||
```js | ||
import { BaseAdapter } from 'i18n'; | ||
class CustomAdapter extends BaseAdapter { | ||
getLocale(language) {} | ||
} | ||
``` | ||
### API | ||
#### I18n | ||
- 构造函数 I18n | ||
@@ -19,6 +71,4 @@ | ||
{ | ||
"autoReload": boolean, // true -> 自动监听 locales文件变化 | ||
"lauguage": string, // 默认语言类型 | ||
"localesBaseDir": string, // locales 文件所在路径,绝对路径 | ||
"localeFileName": { "language": 对应的locales文件名 } // localeFileName存储语言类型到locales文件的映射,该项可选,当不传入时,将自动扫描localesBaseDir目录下文件,并将各个locale文件名作为该文件对应的语言 | ||
"adater": BaseAdapter, // 适配器 | ||
"defaultLanguage": string // 默认语言 | ||
} | ||
@@ -28,12 +78,13 @@ ``` | ||
- setLanguage | ||
* i18n.setLanguage | ||
- 参数: language | ||
- 参数: language, 语言类型 | ||
- 无返回值 | ||
- getLauguage | ||
* i18n.getLauguage | ||
- 无参数 | ||
- 返回当前语言类型 | ||
- translate | ||
* i18n.translate | ||
@@ -52,10 +103,77 @@ - 参数 phrase, args | ||
``` | ||
```js | ||
i18n.translate('report.singular', {cnt: 1}); // only 1 report | ||
i18n.translate('report.singular', { cnt: 1 }); // only 1 report | ||
``` | ||
## License | ||
#### FileSyncAdapter | ||
- 构造函数 FileSyncAdapter | ||
- 参数: options | ||
```json | ||
{ | ||
"localesBaseDir": string, // locales 文件所在路径,绝对路径 | ||
"localeFileName": { "language": 对应的locales文件名 } // localeFileName存储语言类型到locales文件的映射,该项可选,当不传入时,将自动扫描localesBaseDir目录下文件,并将各个locale文件名作为该文件对应的语言 | ||
} | ||
``` | ||
- 返回 FileSyncAdapter 实例 | ||
- fileSyncAdapter.getLocale | ||
- 参数 languag, 语言类型 | ||
- 返回 language 对应的 locale 数据 | ||
#### ObjectAdapter | ||
- 构造函数 ObjectAdapter | ||
- 参数 locales, 保存 locales 信息的对象 | ||
```json | ||
{ | ||
"zh": { | ||
"user": { | ||
"name": "PicGo", | ||
"country": "China" | ||
}, | ||
"report": { | ||
"singular": " ${cnt}个报告", | ||
"plural": "${cnt}个报告" | ||
} | ||
}, | ||
"en": { | ||
"user": { | ||
"name": "PicGo", | ||
"country": "China" | ||
}, | ||
"report": { | ||
"singular": "only ${cnt} report", | ||
"plural": "${cnt} reports" | ||
} | ||
} | ||
} | ||
``` | ||
- 返回 ObjectAdapter 实例 | ||
- objectAdapter.getLocale | ||
- 参数 languag, 语言类型 | ||
- 返回 language 对应的 locale 数据 | ||
* objectAdapter.setLocales 用于动态修改 objectAdapter 上的 locales 数据 | ||
- 参数 locales, locales 数据 | ||
- 无返回值 | ||
```js | ||
objectAdapter.setLocales({ | ||
zh: { | ||
newData: 'this is new Data', | ||
}, | ||
}); | ||
``` | ||
### License | ||
[MIT](http://opensource.org/licenses/MIT) | ||
Copyright (c) 2020 PicGo Group |
@@ -1,3 +0,3 @@ | ||
import { terser } from 'rollup-plugin-terser' | ||
import typescript from 'rollup-plugin-typescript2' | ||
import { terser } from 'rollup-plugin-terser'; | ||
import typescript from 'rollup-plugin-typescript2'; | ||
export default { | ||
@@ -17,5 +17,5 @@ input: './src/index.ts', | ||
format: 'cjs', | ||
file: 'dist/index.js', | ||
file: 'dist/i18n_cjs.js', | ||
sourcemap: false | ||
}] | ||
} | ||
}; |
147
src/i18n.ts
@@ -1,5 +0,4 @@ | ||
import * as fs from 'fs'; | ||
import * as path from 'path'; | ||
import { EFileChangeType, II18nConstructorOptions, ILocaleFileName } from './types/index'; | ||
import { Logger } from './logger'; | ||
import { BaseAdapter } from './adapters'; | ||
import { II18nConstructorOptions } from './types'; | ||
import { logger } from './utils'; | ||
@@ -9,111 +8,45 @@ const DOTNOTATION = '.'; | ||
export class I18n { | ||
private language: string; | ||
private locales: { [locale: string]: any }; // cache locale content | ||
private localeFileName: ILocaleFileName = {}; // localeName -> the name of localeFile | ||
private readonly localesBaseDir: string; | ||
private readonly logger: Logger; | ||
constructor(options: II18nConstructorOptions) { | ||
this.logger = new Logger(); | ||
this.locales = {}; | ||
const { language, localesBaseDir, localeFileName } = options; | ||
this.localesBaseDir = localesBaseDir; | ||
if (localeFileName) { | ||
this.localeFileName = localeFileName; | ||
} else { | ||
this.guessLocaleFileName(localesBaseDir); | ||
} | ||
this.language = language; | ||
this.loadLocale(language); // load locale | ||
if (options.autoReoload) { | ||
this.watch(this.localesBaseDir); | ||
} | ||
} | ||
private adapter: BaseAdapter; | ||
private currentLanguage: string; | ||
constructor(options: II18nConstructorOptions) { | ||
const { adapter, defaultLanguage } = options; | ||
this.adapter = adapter; | ||
this.currentLanguage = defaultLanguage.trim().toLowerCase(); | ||
} | ||
getLanguage(): string { | ||
return this.language; | ||
} | ||
getLanguage(): string { | ||
return this.currentLanguage; | ||
} | ||
setLanguage(language: string) { | ||
this.language = language; | ||
if (!this.locales[language]) { | ||
this.loadLocale(language); | ||
} | ||
} | ||
setLanguage(language: string) { | ||
this.currentLanguage = language.trim().toLowerCase(); | ||
} | ||
translate(phrase: string, args: any) { | ||
const currentLocale = this.locales[this.language]; | ||
if (!currentLocale) { | ||
this.logger.error('current locale is null'); | ||
return; | ||
} | ||
translate(phrase: string, args: any) { | ||
const currentLocale = this.adapter.getLocale(this.currentLanguage); | ||
if (!currentLocale) { | ||
logger.error('current locale is null'); | ||
return; | ||
} | ||
const template = phrase.split(DOTNOTATION).reduce((object: any, key: string) => { | ||
if (!object || !object.hasOwnProperty(key)) { | ||
this.logger.warn(`current locale does\'t contain ${phrase}`); | ||
return; | ||
} | ||
return object[key]; | ||
}, currentLocale); | ||
const template = phrase.split(DOTNOTATION).reduce((object: any, key: string) => { | ||
if (!object || !object.hasOwnProperty(key)) { | ||
logger.warn(`current locale does\'t contain ${phrase}`); | ||
return; | ||
} | ||
return object[key]; | ||
}, currentLocale); | ||
return this.postProcess(template, args); | ||
} | ||
return this.postProcess(template, args); | ||
} | ||
private postProcess(template: string, args: any) { | ||
if (!template) { | ||
return; | ||
} | ||
const keys = Object.keys(args); | ||
const values = keys.map((key) => args[key]); | ||
return new Function(keys.join(','), `return \`${template}\``)(values); | ||
} | ||
private loadLocale(language: string) { | ||
if (!this.localeFileName[language]) { | ||
this.logger.error(`can\'t locate the locale file of language ${language}`); | ||
return; | ||
} | ||
const filePath = path.join(this.localesBaseDir, this.localeFileName[language]); | ||
const fileContent = fs.readFileSync(filePath, { | ||
encoding: 'utf-8', | ||
}); | ||
try { | ||
const locale = JSON.parse(fileContent); | ||
this.locales[language] = locale; // load locale content | ||
} catch (err) { | ||
this.logger.error(`unable to parse locales from file (maybe ${filePath} is empty or invalid json?)`); | ||
this.logger.error(`raw error info: ${err}`); | ||
} | ||
} | ||
// TODO change the name of this method | ||
private guessLocaleFileName(dir: string) { | ||
const files = fs.readdirSync(dir); | ||
const localeFileName: ILocaleFileName = {}; | ||
files.forEach((fileName: string) => { | ||
const localeName = fileName.replace(path.extname(fileName), ''); | ||
localeFileName[localeName] = fileName; | ||
}); | ||
this.logger.log(`guess locale file path from ${dir}`); | ||
this.logger.log(`localeFileName: ${JSON.stringify(localeFileName)}`); | ||
this.localeFileName = localeFileName; | ||
} | ||
// useless ? | ||
private watch(dir: string) { | ||
fs.watch(dir, (eventType: string, fileName: string) => { | ||
let language = ''; | ||
const { localeFileName } = this; | ||
for (const lan in localeFileName) { | ||
if (localeFileName.hasOwnProperty(lan)) { | ||
if (localeFileName[lan] === fileName) { | ||
language = lan; | ||
} | ||
} | ||
} | ||
if (language && eventType === EFileChangeType.change) { | ||
this.loadLocale(language); // update locale | ||
this.logger.log(`${fileName} has updated`); | ||
} | ||
}); | ||
} | ||
private postProcess(template: string, args: any) { | ||
if (!template) { | ||
return; | ||
} | ||
// see benchmark | ||
return Object.keys(args).reduce((res, key) => { | ||
return res.replace('${' + key + '}', args[key]); | ||
}, template); | ||
} | ||
} |
@@ -1,2 +0,3 @@ | ||
export { I18n } from "./i18n"; | ||
export * from './adapters'; | ||
export * from './utils'; | ||
export * from './i18n'; |
@@ -0,1 +1,2 @@ | ||
import { BaseAdapter } from '../adapters'; | ||
export interface ILocaleFileName { | ||
@@ -5,5 +6,16 @@ [locale: string]: string; | ||
export interface ILocale { | ||
[key: string]: any; | ||
} | ||
export interface ILocaleMap { | ||
[language: string]: ILocale; | ||
} | ||
export interface II18nConstructorOptions { | ||
autoReoload?: boolean; | ||
language: string; | ||
adapter: BaseAdapter; | ||
defaultLanguage: string; | ||
} | ||
export interface IFileSyncAdapterConstructorOptions { | ||
localesBaseDir: string; | ||
@@ -16,1 +28,5 @@ localeFileName?: ILocaleFileName; | ||
} | ||
export enum ERUN_ENV { | ||
dev = 'development', | ||
} |
const assert = require('assert'); | ||
const path = require('path'); | ||
const I18n = require('../dist/index').I18n; | ||
const { I18n, FileSyncAdapter, ObjectAdapter } = require('../dist/index'); | ||
const i18n = new I18n({ | ||
autoReoload: true, | ||
language: 'zh', | ||
const fileSyncAdapter = new FileSyncAdapter({ | ||
localesBaseDir: path.resolve(__dirname, './locales'), | ||
}); | ||
describe('i18n', function(){ | ||
const objectAdapter = new ObjectAdapter({ | ||
zh: { | ||
user: { | ||
name: 'PicGo', | ||
country: '中国', | ||
}, | ||
report: { | ||
singular: ' ${cnt}个报告', | ||
plural: '${cnt}个报告', | ||
}, | ||
}, | ||
en: { | ||
user: { | ||
name: 'PicGo', | ||
country: 'China', | ||
}, | ||
report: { | ||
singular: 'only ${cnt} report', | ||
plural: '${cnt} reports', | ||
}, | ||
}, | ||
}); | ||
it('translate', () => { | ||
assert.equal(i18n.translate('report.plural', { cnt: 2 }), '2个报告'); | ||
describe('i18n', () => { | ||
describe('fileSyncAdapter', () => { | ||
const i18n = new I18n({ | ||
adapter: fileSyncAdapter, | ||
defaultLanguage: 'zh', | ||
}); | ||
it('translate', () => { | ||
assert.equal(i18n.translate('report.plural', { cnt: 2 }), '2个报告'); | ||
}); | ||
it('setLanguage', () => { | ||
i18n.setLanguage('en'); | ||
assert.equal(i18n.translate('report.plural', { cnt: 2 }), '2 reports'); | ||
}); | ||
}); | ||
it('setLanguate', () => { | ||
i18n.setLanguage('en'); | ||
assert.equal(i18n.translate('report.plural', { cnt: 2 }), '2 reports'); | ||
describe('objectAdapter', () => { | ||
const i18n = new I18n({ | ||
adapter: objectAdapter, | ||
defaultLanguage: 'zh', | ||
}); | ||
it('translate', () => { | ||
assert.equal(i18n.translate('report.plural', { cnt: 2 }), '2个报告'); | ||
}); | ||
it('setLanguage', () => { | ||
i18n.setLanguage('en'); | ||
assert.equal(i18n.translate('report.plural', { cnt: 2 }), '2 reports'); | ||
}); | ||
it('setLocales', () => { | ||
objectAdapter.setLocales({ | ||
en: { | ||
user: { | ||
name: 'PicGo', | ||
country: 'China', | ||
}, | ||
post: { | ||
singular: 'only ${cnt} post', | ||
plural: '${cnt} posts', | ||
}, | ||
}, | ||
}); | ||
assert.equal(i18n.translate('post.plural', { cnt: 2 }), '2 posts'); | ||
}); | ||
}); | ||
}); | ||
}); |
{ | ||
"user": { | ||
"name": "PicGo", | ||
"counter": "China" | ||
"country": "China" | ||
}, | ||
@@ -6,0 +6,0 @@ "report": { |
{ | ||
"user": { | ||
"name": "PicGo", | ||
"counter": "China" | ||
"country": "China" | ||
}, | ||
@@ -6,0 +6,0 @@ "report": { |
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
28843
41
664
177
2
5
2