Match-id-ba4b273d9e7a1bec972bcb881375b68c44e1d356
This commit is contained in:
commit
edc88e6479
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (c) 2020 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openGauss is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
],
|
||||
root: true,
|
||||
|
||||
plugins: ['jest', 'no-for-of-loops', 'no-function-declare-after-return', 'react', '@typescript-eslint'],
|
||||
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 8,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
modules: true,
|
||||
experimentalObjectRestSpread: true,
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
jest: true,
|
||||
node: true,
|
||||
es6: true,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
semi: ['warn', 'always'],
|
||||
quotes: ['warn', 'single'],
|
||||
'accessor-pairs': 'off',
|
||||
'brace-style': ['error', '1tbs'],
|
||||
'func-style': ['warn', 'declaration', { allowArrowFunctions: true }],
|
||||
'max-lines-per-function': 'off',
|
||||
'object-curly-newline': 'off',
|
||||
// 尾随逗号
|
||||
'comma-dangle': ['error', 'only-multiline'],
|
||||
|
||||
'no-constant-condition': 'off',
|
||||
'no-for-of-loops/no-for-of-loops': 'error',
|
||||
'no-function-declare-after-return/no-function-declare-after-return': 'error',
|
||||
},
|
||||
globals: {
|
||||
isDev: true,
|
||||
isTest: true,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['scripts/tests/**/*.js'],
|
||||
globals: {
|
||||
container: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,15 @@
|
|||
module.exports = {
|
||||
printWidth: 120, // 一行120字符数,如果超过会进行换行
|
||||
tabWidth: 2, // tab等2个空格
|
||||
useTabs: false, // 用空格缩进行
|
||||
semi: true, // 行尾使用分号
|
||||
singleQuote: true, // 字符串使用单引号
|
||||
quoteProps: 'as-needed', // 仅在需要时在对象属性添加引号
|
||||
jsxSingleQuote: false, // 在JSX中使用双引号
|
||||
trailingComma: 'es5', // 使用尾逗号(对象、数组等)
|
||||
bracketSpacing: true, // 对象的括号间增加空格
|
||||
bracketSameLine: false, // 将多行JSX元素的>放在最后一行的末尾
|
||||
arrowParens: 'avoid', // 在唯一的arrow函数参数周围省略括号
|
||||
vueIndentScriptAndStyle: false, // 不缩进Vue文件中的<script>和<style>标记内的代码
|
||||
endOfLine: 'lf', // 仅限换行(\n)
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
# Horizon-Intl
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
const {preset} = require("./jest.config");
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
browsers: ['> 1%', 'last 2 versions', 'not ie <= 8'],
|
||||
node: 'current',
|
||||
},
|
||||
useBuiltIns: 'usage',
|
||||
corejs: 3,
|
||||
},
|
||||
],
|
||||
[
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
"runtime": "automatic",
|
||||
"importSource": "@cloudsop/horizon"
|
||||
}
|
||||
]
|
||||
],
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import Horizon, { useState } from '@cloudsop/horizon';
|
||||
import { IntlProvider } from "../index";
|
||||
import zh from "./locale/zh";
|
||||
import en from "./locale/en";
|
||||
import Example1 from "./components/Example1";
|
||||
import Example2 from "./components/Example2";
|
||||
import Example3 from "./components/Example3";
|
||||
import Example4 from "./components/Example4";
|
||||
import Example5 from "./components/Example5";
|
||||
import Example6 from "./components/Example6";
|
||||
|
||||
const App = () => {
|
||||
const [locale, setLocale] = useState('zh');
|
||||
const handleChange = () => {
|
||||
locale === 'zh' ? setLocale('en') : setLocale('zh');
|
||||
};
|
||||
const message = locale === 'zh' ? zh : en
|
||||
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={locale === 'zh' ? zh : en}>
|
||||
<header>Horizon-Intl API Test Demo</header>
|
||||
|
||||
<div className='container'>
|
||||
<Example1/>
|
||||
<Example2/>
|
||||
<Example3/>
|
||||
</div>
|
||||
<div className='container'>
|
||||
<Example4 locale={locale} messages={message}/>
|
||||
<Example5/>
|
||||
<Example6 locale={{ locale }} messages={message}/>
|
||||
</div>
|
||||
<div className='button'>
|
||||
<button onClick={handleChange}>切换语言</button>
|
||||
</div>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import Horizon from "@cloudsop/horizon";
|
||||
import { useIntl } from "../../index";
|
||||
|
||||
const Example1 = () => {
|
||||
const { i18n } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>useIntl方式测试Demo</h2>
|
||||
<pre>{i18n.formatMessage({ id: 'text1' })}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Example1;
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import Horizon from "@cloudsop/horizon";
|
||||
import { FormattedMessage } from "../../index";
|
||||
|
||||
const Example2= () => {
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>FormattedMessage方式测试Demo</h2>
|
||||
<pre>
|
||||
<FormattedMessage id='text2'/>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Example2;
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import Horizon from '@cloudsop/horizon';
|
||||
import { FormattedMessage } from "../../index";
|
||||
|
||||
const Example3 = (props) => {
|
||||
const { locale, setLocale } = props;
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>FormattedMessage方式测试Demo</h2>
|
||||
<pre>
|
||||
<button className="testButton" onClick={() => {
|
||||
setLocale(locale === 'zh' ? 'en' : 'zh')
|
||||
}}>
|
||||
<FormattedMessage id={'button'}/>
|
||||
</button>
|
||||
<br/>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Example3;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import Horizon from "@cloudsop/horizon";
|
||||
import { createIntl } from "../../index";
|
||||
|
||||
const Example4 = (props) => {
|
||||
// 受渲染时机影响,createIntl方式需控制时序,否则慢一拍
|
||||
const intl = createIntl({ ...props });
|
||||
const msg = intl.formatMessage({ id: 'text3' });
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>createIntl方式测试Demo</h2>
|
||||
<pre>{msg}</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Example4;
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import Horizon, { Component } from '@cloudsop/horizon';
|
||||
import { injectIntl } from '../../index';
|
||||
|
||||
class Example5 extends Component<any, any, any> {
|
||||
public constructor(props: any, context) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { intl } = this.props as any;
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>injectIntl方式测试Demo</h2>
|
||||
<pre>{intl.formatMessage({ id: 'text4' })}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(Example5);
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import Horizon from "@cloudsop/horizon";
|
||||
import { createIntl, createIntlCache, RawIntlProvider } from "../../index";
|
||||
import Example6Child from "./Example6Child";
|
||||
|
||||
const Example6 = (props: any) => {
|
||||
|
||||
const { locale, messages } = props;
|
||||
|
||||
const cache = createIntlCache();
|
||||
let i18n = createIntl(
|
||||
{ locale: locale, messages: messages },
|
||||
cache
|
||||
);
|
||||
|
||||
return (
|
||||
<RawIntlProvider value={i18n}>
|
||||
<Example6Child/>
|
||||
</RawIntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default Example6;
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import { useIntl } from "../../index";
|
||||
|
||||
const Example6Child = (props: any) => {
|
||||
|
||||
const {formatMessage} = useIntl();
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<h2>RawIntlProvider方式测试Demo</h2>
|
||||
<pre>{formatMessage({ id: 'text4' })}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Example6Child;
|
|
@ -0,0 +1,102 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Horizon-Intl API Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f8f8f8;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80px;
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
color: #2c3e50;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 5px #aaa;
|
||||
width: 400px;
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
box-shadow: 0 0 15px #aaa;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.card pre {
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
font-size: 14px;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.testButton {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background-color: #007bff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import * as Horizon from '@cloudsop/horizon';
|
||||
import App from './App'
|
||||
|
||||
function render() {
|
||||
Horizon.render(
|
||||
<>
|
||||
<App/>
|
||||
</>,
|
||||
document.querySelector('#root') as any
|
||||
)
|
||||
}
|
||||
render();
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
export default {
|
||||
button: 'Welcome to the Horizon-Intl component!',
|
||||
text1: 'Welcome to the Horizon-Intl component!',
|
||||
text2: 'Welcome to the Horizon-Intl component!',
|
||||
text3: 'Welcome to the Horizon-Intl component!',
|
||||
text4: 'Welcome to the Horizon-Intl component!',
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
export default {
|
||||
button: '欢迎使用国际化组件!',
|
||||
text1: '欢迎使用国际化组件!',
|
||||
text2: '欢迎使用国际化组件!',
|
||||
text3: '欢迎使用国际化组件!',
|
||||
text4: '欢迎使用国际化组件!',
|
||||
};
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import DateTimeFormatter from './src/format/fomatters/DateTimeFormatter';
|
||||
import NumberFormatter from './src/format/fomatters/NumberFormatter';
|
||||
import I18n from './src/core/I18n';
|
||||
import createI18nCache from './src/format/cache/cache';
|
||||
import FormattedMessage from './src/core/components/FormattedMessage';
|
||||
import I18nProvider from './src/core/components/I18nProvider';
|
||||
import injectIntl, { I18nContext, InjectProvider } from './src/core/components/InjectI18n';
|
||||
import useI18n from './src/core/hook/useI18n';
|
||||
import createI18n from './src/core/createI18n';
|
||||
import { InjectedIntl, MessageDescriptor } from './src/types/interfaces';
|
||||
// 函数API
|
||||
export {
|
||||
I18n,
|
||||
createI18nCache as createIntlCache,
|
||||
createI18n as createIntl,
|
||||
DateTimeFormatter,
|
||||
NumberFormatter,
|
||||
useI18n as useIntl,
|
||||
};
|
||||
|
||||
// 组件
|
||||
export {
|
||||
FormattedMessage,
|
||||
I18nContext,
|
||||
I18nProvider as IntlProvider,
|
||||
injectIntl as injectIntl,
|
||||
InjectProvider as RawIntlProvider,
|
||||
};
|
||||
|
||||
// 用于定义文本
|
||||
export function defineMessages<K extends keyof any, T = MessageDescriptor, U = Record<K, T>>(msgs: U): U {
|
||||
return msgs;
|
||||
}
|
||||
|
||||
export function defineMessage<T>(msg: T): T {
|
||||
return msg;
|
||||
}
|
||||
|
||||
export interface InjectedIntlProps {
|
||||
intl: InjectedIntl;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
module.exports = {
|
||||
coverageDirectory: 'coverage',
|
||||
resetModules: true,
|
||||
preset: 'ts-jest/presets/js-with-ts',
|
||||
rootDir: process.cwd(),
|
||||
testMatch: [
|
||||
'<rootDir>/tests/**/*.test.[jt]s?(x)'
|
||||
],
|
||||
moduleFileExtensions: ['ts', 'js', 'jsx', 'tsx'],
|
||||
moduleDirectories: ['node_modules', 'src'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/tests/$1',
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.(ts|js|jsx|tsx)$": "babel-jest"
|
||||
},
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
"tsconfig": "tsconfig.json"
|
||||
}
|
||||
},
|
||||
|
||||
testEnvironment: 'jsdom',
|
||||
};
|
|
@ -0,0 +1,63 @@
|
|||
{
|
||||
"name": "@cloudsop/horizon-intl",
|
||||
"version": "1.0.7",
|
||||
"description": "",
|
||||
"main": "build/intl.umd.js",
|
||||
"type": "commonjs",
|
||||
"types": "build/@types/index.d.ts",
|
||||
"scripts": {
|
||||
"demo-serve": "webpack serve --mode=development",
|
||||
"rollup-build": "rollup --config rollup.config.js",
|
||||
"test": "jest --config jest.config.js",
|
||||
"test-c": "jest --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://szv-open.codehub.huawei.com/innersource/fenghuang/horizon/horizon-ecosystem.git"
|
||||
},
|
||||
"files": [
|
||||
"build",
|
||||
"example"
|
||||
],
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@cloudsop/horizon": "^0.0.46",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.3",
|
||||
"@babel/preset-env": "^7.16.7",
|
||||
"@babel/preset-react": "^7.9.4",
|
||||
"@babel/preset-typescript": "7.16.7",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-node-resolve": "^7.1.3",
|
||||
"@rollup/plugin-typescript": "^11.0.0",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/node": "^16.18.27",
|
||||
"@types/react": "18.0.25",
|
||||
"babel": "^6.23.0",
|
||||
"babel-jest": "^29.5.0",
|
||||
"babel-loader": "^9.1.2",
|
||||
"core-js": "3.31.0",
|
||||
"html-webpack-plugin": "^5.5.1",
|
||||
"jest": "29.3.1",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"prettier": "^2.8.7",
|
||||
"rollup": "^2.0.0",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
"rollup-plugin-serve": "^1.1.0",
|
||||
"rollup-plugin-terser": "^5.3.0",
|
||||
"tslib": "^2.6.1",
|
||||
"ts-jest": "29.0.3",
|
||||
"ts-node": "10.9.1",
|
||||
"typescript": "4.9.3",
|
||||
"webpack": "^5.81.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.13.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import babel from '@rollup/plugin-babel';
|
||||
import nodeResolve from '@rollup/plugin-node-resolve';
|
||||
import typescript from "@rollup/plugin-typescript";
|
||||
import { terser } from 'rollup-plugin-terser';
|
||||
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const entry = path.join(__dirname, '/index.ts');
|
||||
|
||||
const output = path.join(__dirname, '/build');
|
||||
|
||||
const extensions = ['.js', '.ts', '.tsx'];
|
||||
|
||||
export default {
|
||||
input: entry,
|
||||
output: [
|
||||
{
|
||||
file: path.resolve(output, 'intl.umd.js'),
|
||||
sourcemap: 'inline',
|
||||
name: 'I18n',
|
||||
format: 'umd',
|
||||
},
|
||||
],
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
extensions,
|
||||
modulesOnly: true,
|
||||
}),
|
||||
babel({
|
||||
exclude: 'node_modules/**',
|
||||
configFile: path.join(__dirname, '/babel.config.js'),
|
||||
extensions,
|
||||
}),
|
||||
typescript(
|
||||
{
|
||||
tsconfig: 'tsconfig.json',
|
||||
include: ['./**/*.ts', './**/*.tsx'],
|
||||
}
|
||||
),
|
||||
terser(),
|
||||
],
|
||||
external:[
|
||||
"@cloudsop/horizon",
|
||||
"react",
|
||||
"react-dom"
|
||||
]
|
||||
};
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
/**
|
||||
* \\(?:u\{[a-fA-F0-9]+}) 匹配形如 \u{0020} 的 Unicode 转义字符。
|
||||
* \\x[a-fA-F0-9]{2} 匹配形如 \x0A 的十六进制转义字符。
|
||||
* [nrtf'"] 匹配常见的转义字符:\n(换行符)、\r(回车符)、\t(制表符)、\f(换页符)、\'(单引号)和 \"(双引号)。
|
||||
*/
|
||||
export const UNICODE_REG = /\\(?:u\{[a-fA-F0-9]+}|x[a-fA-F0-9]{2}|[nrtf'"])/g;
|
||||
|
||||
// Horizon需要被保留静态常量
|
||||
export const HORIZON_STATICS = {
|
||||
childContextTypes: true,
|
||||
contextType: true,
|
||||
contextTypes: true,
|
||||
defaultProps: true,
|
||||
displayName: true,
|
||||
getDefaultProps: true,
|
||||
getDerivedStateFromError: true,
|
||||
getDerivedStateFromProps: true,
|
||||
mixins: true,
|
||||
propTypes: true,
|
||||
type: true,
|
||||
};
|
||||
|
||||
// JavaScript 需要被保留原生静态属性
|
||||
export const NATIVE_STATICS = {
|
||||
name: true,
|
||||
length: true,
|
||||
prototype: true,
|
||||
caller: true,
|
||||
callee: true,
|
||||
arguments: true,
|
||||
arity: true,
|
||||
};
|
||||
|
||||
// Horizon ForwardRef 组件的静态属性需要被保留
|
||||
export const HORIZON_FORWARD_REF_STATICS = {
|
||||
vtype: true,
|
||||
render: true,
|
||||
defaultProps: true,
|
||||
key: true,
|
||||
type: true,
|
||||
};
|
||||
|
||||
// React ForwardRef 组件的静态属性需要被保留
|
||||
export const REACT_FORWARD_REF_STATICS = {
|
||||
$$typeof: true, // horizon 'vtype': true
|
||||
render: true, // render
|
||||
defaultProps: true, // props
|
||||
displayName: true,
|
||||
propTypes: true, // type: type,
|
||||
};
|
||||
|
||||
export const FORWARD_REF_STATICS = {...HORIZON_FORWARD_REF_STATICS, ...REACT_FORWARD_REF_STATICS};
|
||||
|
||||
// Horizon Memo 组件的静态属性需要被保留
|
||||
export const HORIZON_MEMO_STATICS = {
|
||||
vtype: true, // horizon 'vtype': true
|
||||
compare: true,
|
||||
defaultProps: true,
|
||||
type: true,
|
||||
};
|
||||
|
||||
// 默认复数规则
|
||||
export const DEFAULT_PLURAL_KEYS = ['zero', 'one', 'two', 'few', 'many', 'other'];
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import EventDispatcher from '../utils/eventListener';
|
||||
import DateTimeFormatter from "../format/fomatters/DateTimeFormatter";
|
||||
import NumberFormatter from "../format/fomatters/NumberFormatter";
|
||||
import { getFormatMessage } from '../format/getFormatMessage';
|
||||
import { I18nProps, MessageDescriptor, MessageOptions } from '../types/interfaces';
|
||||
import { Locale, Locales, Messages, AllLocaleConfig, AllMessages, LocaleConfig, Error, Events } from '../types/types';
|
||||
|
||||
export class I18n extends EventDispatcher<Events> {
|
||||
public locale: Locale;
|
||||
public locales: Locales;
|
||||
private readonly _localeConfig: AllLocaleConfig;
|
||||
private readonly allMessages: AllMessages;
|
||||
public readonly error?: Error;
|
||||
public readonly useMemorize?: boolean;
|
||||
|
||||
constructor(props: I18nProps) {
|
||||
super();
|
||||
this.locale = 'en';
|
||||
this.locales = this.locale || '';
|
||||
this.allMessages = {};
|
||||
this._localeConfig = {};
|
||||
this.error = props.error;
|
||||
|
||||
this.loadMessage(props.messages);
|
||||
|
||||
if (props.localeConfig) {
|
||||
this.loadLocaleConfig(props.localeConfig);
|
||||
}
|
||||
|
||||
if (props.locale || props.locales) {
|
||||
this.changeLanguage(props.locale!, props.locales);
|
||||
}
|
||||
this.formatMessage = this.formatMessage.bind(this);
|
||||
this.formatDate = this.formatDate.bind(this);
|
||||
this.formatNumber = this.formatNumber.bind(this);
|
||||
}
|
||||
|
||||
get messages(): string | Messages | AllMessages {
|
||||
if (this.locale in this.allMessages) {
|
||||
return this.allMessages[this.locale] ?? {};
|
||||
} else {
|
||||
return this.allMessages ?? {};
|
||||
}
|
||||
}
|
||||
|
||||
get localeConfig(): LocaleConfig {
|
||||
return this._localeConfig[this.locale] ?? {};
|
||||
}
|
||||
|
||||
setLocaleConfig(locale: Locale, localeData: LocaleConfig) {
|
||||
if (this._localeConfig[locale]) {
|
||||
Object.assign(this._localeConfig, localeData);
|
||||
} else {
|
||||
this._localeConfig[locale] = localeData;
|
||||
}
|
||||
}
|
||||
|
||||
// 将热语言环境的本地化数据加载
|
||||
loadLocaleConfig(localeOrAllData: Locale | AllLocaleConfig, localeConfig?: LocaleConfig) {
|
||||
if (localeConfig) {
|
||||
this.setLocaleConfig(localeOrAllData as Locale, localeConfig);
|
||||
} else {
|
||||
Object.keys(localeOrAllData).forEach(locale => {
|
||||
this.setLocaleConfig(locale, localeOrAllData[locale]);
|
||||
});
|
||||
}
|
||||
this.emit('change');
|
||||
}
|
||||
|
||||
setMessage(locale: Locale, messages: Messages) {
|
||||
if (this.allMessages[locale]) {
|
||||
Object.assign(this.allMessages[locale], messages);
|
||||
} else {
|
||||
this.allMessages[locale] = messages;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载messages
|
||||
loadMessage(localeOrMessages: Locale | AllMessages | undefined, messages?: Messages) {
|
||||
if (messages) {
|
||||
//当 message 为空的时候,加载单一的message信息
|
||||
this.setMessage(localeOrMessages as string, messages);
|
||||
} else {
|
||||
// 加载多对locale-message信息
|
||||
localeOrMessages &&
|
||||
Object.keys(localeOrMessages!).forEach(locale => this.setMessage(locale, localeOrMessages![locale]));
|
||||
}
|
||||
this.emit('change');
|
||||
}
|
||||
|
||||
// 改变当前的语言环境
|
||||
changeLanguage(locale: Locale, locales?: Locales) {
|
||||
this.locale = locale;
|
||||
if (locales) {
|
||||
this.locales = locales;
|
||||
}
|
||||
this.emit('change');
|
||||
}
|
||||
|
||||
formatMessage(
|
||||
id: MessageDescriptor | string,
|
||||
values: Object | undefined = {},
|
||||
{ message, context, formatOptions, useMemorize}: MessageOptions = {},
|
||||
) {
|
||||
return getFormatMessage(this, id, values, { message, context, formatOptions, useMemorize});
|
||||
}
|
||||
|
||||
formatDate(value: string | Date, formatOptions?: Intl.DateTimeFormatOptions): string {
|
||||
const dateTimeFormatter = new DateTimeFormatter(this.locale || this.locales, formatOptions, this.useMemorize);
|
||||
return dateTimeFormatter.dateTimeFormat(value);
|
||||
}
|
||||
|
||||
formatNumber(value: number, formatOptions?: Intl.NumberFormatOptions): string {
|
||||
const numberFormatter = new NumberFormatter(this.locale || this.locales, formatOptions, this.useMemorize);
|
||||
return numberFormatter.numberFormat(value);
|
||||
}
|
||||
}
|
||||
|
||||
export default I18n;
|
||||
|
||||
export function createI18nInstance(i18nProps: I18nProps = {}): I18n {
|
||||
return new I18n(i18nProps);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import Horizon, { Children, Fragment } from '@cloudsop/horizon';
|
||||
import { FormattedMessageProps } from '../../types/interfaces';
|
||||
import useI18n from '../hook/useI18n';
|
||||
|
||||
/**
|
||||
* FormattedMessage组件,接收一个消息键作为属性,并根据当前选择的语言环境,从对应的翻译资源中获取相应的消息文本,并可选地对文本进行格式化。
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
function FormattedMessage(props: FormattedMessageProps) {
|
||||
const { i18n } = useI18n();
|
||||
const { id, values, messages, formatOptions, context, tagName: TagName = Fragment, children, comment, useMemorize }: any = props;
|
||||
|
||||
const formatMessageOptions = {
|
||||
comment,
|
||||
messages,
|
||||
context,
|
||||
useMemorize,
|
||||
formatOptions,
|
||||
};
|
||||
|
||||
let formattedMessage = i18n.formatMessage(id, values, formatMessageOptions);
|
||||
|
||||
if (typeof children === 'function') {
|
||||
const childNodes = Array.isArray(formattedMessage) ? formattedMessage : [formattedMessage];
|
||||
return children(childNodes);
|
||||
}
|
||||
|
||||
if (TagName) {
|
||||
return (
|
||||
<TagName>{Children.toArray(formattedMessage)}</TagName>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>{formattedMessage}</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FormattedMessage;
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import Horizon, {useRef, useState, useEffect, useMemo, Component} from '@cloudsop/horizon';
|
||||
import utils from '../../utils/utils';
|
||||
import { InjectProvider } from './InjectI18n';
|
||||
import { I18nProviderProps} from '../../types/interfaces';
|
||||
import I18n, {createI18nInstance} from "../I18n";
|
||||
|
||||
/**
|
||||
* 用于为应用程序提供国际化的格式化功能,管理程序中的语言文本信息和本地化资源信息
|
||||
* @param props
|
||||
* @constructor
|
||||
*/
|
||||
const I18nProvider = (props: I18nProviderProps)=> {
|
||||
const { locale, messages, children } = props;
|
||||
|
||||
const i18n = useMemo(() => {
|
||||
return createI18nInstance({
|
||||
locale: locale,
|
||||
messages: messages,
|
||||
});
|
||||
}, [locale, messages]);
|
||||
|
||||
// 使用useRef保存上次的locale值
|
||||
const localeRef = useRef<string | undefined>(i18n.locale);
|
||||
|
||||
const [context, setContext] = useState<I18n>(i18n);
|
||||
|
||||
useEffect(() => {
|
||||
const handleChange = () => {
|
||||
if (localeRef.current !== i18n.locale) {
|
||||
localeRef.current = i18n.locale;
|
||||
setContext(i18n);
|
||||
}
|
||||
};
|
||||
let removeListener = i18n.on('change', handleChange);
|
||||
|
||||
// 手动触发一次 handleChange,以确保 context 的正确性
|
||||
handleChange();
|
||||
|
||||
// 在组件卸载时取消事件监听
|
||||
return () => {
|
||||
removeListener();
|
||||
};
|
||||
}, [i18n]);
|
||||
|
||||
// 提供一个Provider组件
|
||||
return <InjectProvider value={context}>{children}</InjectProvider>;
|
||||
};
|
||||
|
||||
export default I18nProvider;
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import Horizon , { createContext, forwardRef } from '@cloudsop/horizon';
|
||||
import { isVariantI18n } from '../../utils/utils';
|
||||
import copyStaticProps from '../../utils/copyStaticProps';
|
||||
import { InjectOptions } from '../../types/interfaces';
|
||||
import I18n from "../I18n";
|
||||
|
||||
// 创建国际化组件对象上下文
|
||||
export const I18nContext : any = createContext<I18n>(null as any);
|
||||
const { Consumer, Provider } = I18nContext;
|
||||
export const InjectProvider = Provider;
|
||||
|
||||
/**
|
||||
* 用于实现国际化的高阶组件,将国际化功能注入到组件中,使组件能够使用国际化的本文格式化功能
|
||||
* @param Component
|
||||
* @param options
|
||||
*/
|
||||
function injectI18n(Component, options?: InjectOptions): any {
|
||||
const {
|
||||
isUsingForwardRef = false, // 默认不使用
|
||||
} = options || {};
|
||||
|
||||
// 定义一个名为 WrappedI18n 的函数组件,接收传入组件的 props 和 forwardedRef,返回传入组件并注入 i18n
|
||||
const WrappedI18n = props => (
|
||||
<Consumer>
|
||||
{context => {
|
||||
isVariantI18n(context);
|
||||
|
||||
const i18nProps = {
|
||||
intl: context,
|
||||
formatMessage: context.formatMessage,
|
||||
formatDate: context.DateTimeFormat,
|
||||
formatNumber: context.NumberFormat,
|
||||
};
|
||||
return <Component {...props} {...i18nProps} ref={isUsingForwardRef ? props.forwardedRef : null} />;
|
||||
}}
|
||||
</Consumer>
|
||||
);
|
||||
|
||||
WrappedI18n.WrappedComponent = Component;
|
||||
|
||||
// 通过copyStatics方法,复制组件中的静态属性
|
||||
return copyStaticProps(
|
||||
isUsingForwardRef ?
|
||||
forwardRef((props, ref) => <WrappedI18n {...props} forwardedRef={ref} />) :
|
||||
WrappedI18n,
|
||||
Component
|
||||
);
|
||||
}
|
||||
|
||||
export default injectI18n;
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import {I18nCache, I18nProviderProps} from '../types/interfaces';
|
||||
import I18n, {createI18nInstance} from './I18n';
|
||||
|
||||
/**
|
||||
* createI18n hook函数,用于创建国际化i8n实例,以进行相关的数据操作
|
||||
*/
|
||||
|
||||
export const createI18n = (config: I18nProviderProps, cache?: I18nCache): I18n => {
|
||||
const { locale, defaultLocale, messages } = config;
|
||||
return createI18nInstance({
|
||||
locale: locale || defaultLocale || 'en',
|
||||
messages: messages,
|
||||
useMemorize: !!cache,
|
||||
});
|
||||
};
|
||||
|
||||
export default createI18n;
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import Horizon, { useContext } from '@cloudsop/horizon';
|
||||
import utils from '../../utils/utils';
|
||||
import { I18nContext } from '../components/InjectI18n';
|
||||
import I18n from "../I18n";
|
||||
|
||||
/**
|
||||
* useI18n hook,与Horizon组件一起使用。
|
||||
* 使用useI18n钩子函数可以更方便地在函数组件中进行国际化操作
|
||||
*/
|
||||
function useI18n() {
|
||||
const i18nContext = useContext<I18n>(I18nContext);
|
||||
utils.isVariantI18n(i18nContext);
|
||||
const i18n = i18nContext;
|
||||
return {
|
||||
i18n: i18n,
|
||||
formatMessage: i18n.formatMessage.bind(i18n),
|
||||
formatNumber: i18n.formatNumber.bind(i18n),
|
||||
formatDate: i18n.formatDate.bind(i18n),
|
||||
};
|
||||
}
|
||||
|
||||
export default useI18n;
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import { UNICODE_REG } from '../constants';
|
||||
import { CompiledMessage, Locale, LocaleConfig, Locales } from '../types/types';
|
||||
import generateFormatters from './generateFormatters';
|
||||
import { FormatOptions } from '../types/interfaces';
|
||||
|
||||
/**
|
||||
* 获取翻译结果
|
||||
*/
|
||||
class Translation {
|
||||
private readonly compiledMessage: CompiledMessage;
|
||||
private readonly locale: Locale;
|
||||
private readonly locales: Locales;
|
||||
private readonly localeConfig: Record<string, any>;
|
||||
private readonly useMemorize?: boolean;
|
||||
|
||||
constructor(compiledMessage, locale, locales, localeConfig, memorize?) {
|
||||
this.compiledMessage = compiledMessage;
|
||||
this.locale = locale;
|
||||
this.locales = locales;
|
||||
this.localeConfig = localeConfig;
|
||||
this.useMemorize = memorize ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param values 需要替换文本占位符的值
|
||||
* @param formatOptions 需要格式化选项
|
||||
*/
|
||||
translate(values: object, formatOptions: FormatOptions = {}): string {
|
||||
const createTextFormatter = (
|
||||
locale: Locale,
|
||||
locales: Locales,
|
||||
values: object,
|
||||
formatOptions: FormatOptions,
|
||||
localeConfig: LocaleConfig,
|
||||
useMemorize?: boolean
|
||||
) => {
|
||||
const textFormatter = (name: string, type: string, format: any) => {
|
||||
const formatters = generateFormatters(locale, locales, localeConfig, formatOptions);
|
||||
const value = values[name];
|
||||
const formatter = formatters[type](value, format, useMemorize);
|
||||
|
||||
let message;
|
||||
if (typeof formatter === 'function') {
|
||||
message = formatter(textFormatter); // 递归调用
|
||||
} else {
|
||||
message = formatter; // 获得变量值 formatted: "Fred"
|
||||
}
|
||||
|
||||
return Array.isArray(message) ? message.join('') : message;
|
||||
};
|
||||
|
||||
return textFormatter;
|
||||
};
|
||||
|
||||
let textFormatter = createTextFormatter(
|
||||
this.locale,
|
||||
this.locales,
|
||||
values,
|
||||
formatOptions,
|
||||
this.localeConfig,
|
||||
this.useMemorize
|
||||
);
|
||||
// 通过递归方法formatCore进行格式化处理
|
||||
const result = this.formatMessage(this.compiledMessage, textFormatter);
|
||||
return result; // 返回要格式化的结果
|
||||
}
|
||||
|
||||
formatMessage(compiledMessage: CompiledMessage, textFormatter: Function) {
|
||||
if (!Array.isArray(compiledMessage)) {
|
||||
return compiledMessage;
|
||||
}
|
||||
|
||||
return compiledMessage.map(token => {
|
||||
if (typeof token === 'string') {
|
||||
return token;
|
||||
}
|
||||
|
||||
const [name, type, format] = token;
|
||||
|
||||
|
||||
let replaceValueFormat = format;
|
||||
|
||||
// 如果 format 是对象,函数将递归地对它的每个值调用 formatMessage 后保存,否则直接保存
|
||||
if (format && typeof format !== 'string') {
|
||||
replaceValueFormat = Object.keys(replaceValueFormat).reduce((text, key) => {
|
||||
text[key] = this.formatMessage(format[key], textFormatter);
|
||||
return text;
|
||||
}, {});
|
||||
}
|
||||
//调用 getContent 函数来获取给定 name、type 和 interpolateFormat 的值
|
||||
const value = textFormatter(name, type, replaceValueFormat);
|
||||
return value ?? '';
|
||||
}).join('');
|
||||
};
|
||||
}
|
||||
|
||||
export default Translation;
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import { I18nCache } from '../../types/interfaces';
|
||||
|
||||
/**
|
||||
* 缓存机制
|
||||
*/
|
||||
function creatI18nCache(): I18nCache {
|
||||
return {
|
||||
dateTimeFormat: {},
|
||||
numberFormat: {},
|
||||
plurals: {},
|
||||
messages: {},
|
||||
select: {},
|
||||
octothorpe: {},
|
||||
};
|
||||
}
|
||||
|
||||
export default creatI18nCache;
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import creatI18nCache from '../cache/cache';
|
||||
import utils from '../../utils/utils';
|
||||
import { DatePool, Locales } from '../../types/types';
|
||||
|
||||
/**
|
||||
* 时间格式化
|
||||
*/
|
||||
class DateTimeFormatter {
|
||||
private readonly locales: Locales;
|
||||
private readonly formatOptions: Intl.DateTimeFormatOptions;
|
||||
|
||||
// 是否进行存储
|
||||
private readonly useMemorize: boolean;
|
||||
|
||||
// 创建一个缓存对象,用于存储DateTimeFormat的对象
|
||||
private cache = creatI18nCache().dateTimeFormat;
|
||||
|
||||
constructor(locales: Locales, formatOptions?: Intl.DateTimeFormatOptions, useMemorize?: boolean) {
|
||||
this.locales = locales;
|
||||
this.formatOptions = formatOptions ?? {};
|
||||
this.useMemorize = useMemorize ?? true;
|
||||
}
|
||||
|
||||
dateTimeFormat(value: DatePool, formatOptions?: Intl.DateTimeFormatOptions): string {
|
||||
const options = formatOptions ?? this.formatOptions;
|
||||
const formatter = new Intl.DateTimeFormat(this.locales, options);
|
||||
// 将传输的字符串转变为日期对象
|
||||
if (typeof value === 'string') {
|
||||
value = new Date(value);
|
||||
}
|
||||
|
||||
// 如果启用了记忆化且已经有对应的数字格式化器缓存,则直接返回缓存中的格式化结果。否则创建新的格式化数据,并进行缓存
|
||||
if (this.useMemorize) {
|
||||
// 造缓存的key,key包含区域设置和日期时间格式选项
|
||||
const cacheKey = utils.generateKey<Intl.DateTimeFormatOptions>(this.locales, options);
|
||||
|
||||
if (this.cache[cacheKey]) {
|
||||
return this.cache[cacheKey].format(value);
|
||||
}
|
||||
|
||||
// 查询缓存中的key, 若无key则创建新key
|
||||
this.cache[cacheKey] = formatter;
|
||||
return formatter.format(value);
|
||||
}
|
||||
|
||||
// 返回格式化后的时间
|
||||
|
||||
return formatter.format(value);
|
||||
}
|
||||
}
|
||||
|
||||
export default DateTimeFormatter;
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import creatI18nCache from '../cache/cache';
|
||||
import { Locales } from '../../types/types';
|
||||
import utils from '../../utils/utils';
|
||||
|
||||
/**
|
||||
* 数字格式化
|
||||
*/
|
||||
class NumberFormatter {
|
||||
private readonly locales: Locales;
|
||||
private readonly formatOption?: Intl.NumberFormatOptions;
|
||||
private readonly useMemorize?: boolean;
|
||||
private cache = creatI18nCache().numberFormat; // 创建一个缓存对象,用于缓存已经创建的数字格式化器
|
||||
|
||||
constructor(locales: Locales, formatOption?: Intl.NumberFormatOptions, useMemorize?: boolean) {
|
||||
this.locales = locales;
|
||||
this.formatOption = formatOption ?? {};
|
||||
this.useMemorize = useMemorize ?? true;
|
||||
}
|
||||
|
||||
numberFormat(value: number, formatOption?: Intl.NumberFormatOptions): string {
|
||||
const options = formatOption ?? this.formatOption;
|
||||
const formatter = new Intl.NumberFormat(this.locales, options);
|
||||
|
||||
// 如果启用了记忆化且已经有对应的数字格式化器缓存,则直接返回缓存中的格式化结果。否则创建新的格式化数据,并进行缓存
|
||||
if (this.useMemorize) {
|
||||
// 造缓存的key,key包含区域设置数字格式选项
|
||||
const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locales, options);
|
||||
|
||||
if (this.cache[cacheKey]) {
|
||||
return this.cache[cacheKey].format(value);
|
||||
}
|
||||
|
||||
this.cache[cacheKey] = formatter;
|
||||
return formatter.format(value);
|
||||
}
|
||||
return formatter.format(value);
|
||||
}
|
||||
}
|
||||
|
||||
export default NumberFormatter;
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import utils from '../../utils/utils';
|
||||
import NumberFormatter from './NumberFormatter';
|
||||
import { Locale, Locales } from '../../types/types';
|
||||
|
||||
/**
|
||||
* 复数格式化
|
||||
*/
|
||||
class PluralFormatter {
|
||||
private readonly locale: Locale;
|
||||
private readonly locales: Locales;
|
||||
private readonly value: number;
|
||||
private readonly message: any;
|
||||
private readonly useMemorize: boolean;
|
||||
private octothorpe: Record<string, any> = {};
|
||||
|
||||
constructor(locale, locales, value, message, useMemorize?) {
|
||||
this.locale = locale;
|
||||
this.locales = locales;
|
||||
this.value = value;
|
||||
this.message = message;
|
||||
this.useMemorize = useMemorize ?? true;
|
||||
}
|
||||
|
||||
// 将 message中的“#”替换为指定数字value,并返回新的字符串或者字符串数组
|
||||
replaceSymbol(ctx: any) {
|
||||
const msg = typeof this.message === 'function' ? this.message(ctx) : this.message;
|
||||
const messages = Array.isArray(msg) ? msg : [msg];
|
||||
|
||||
const numberFormatter = new NumberFormatter(this.locales);
|
||||
const valueStr = numberFormatter.numberFormat(this.value);
|
||||
|
||||
if (this.useMemorize) {
|
||||
// 创建key,用于唯一标识
|
||||
const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locale, this.message);
|
||||
|
||||
// 如果key存在,则使用缓存中的替代
|
||||
if (this.octothorpe[cacheKey]) {
|
||||
return messages.map(msg => (typeof msg === 'string' ? msg.replace('#', this.octothorpe[cacheKey]) : msg));
|
||||
}
|
||||
|
||||
// 如果不存在,则进行缓存
|
||||
this.octothorpe[cacheKey] = valueStr;
|
||||
}
|
||||
|
||||
return messages.map(msg => (typeof msg === 'string' ? msg.replace('#', valueStr) : msg));
|
||||
}
|
||||
}
|
||||
|
||||
export default PluralFormatter;
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import utils from '../../utils/utils';
|
||||
import { Locale } from '../../types/types';
|
||||
|
||||
/**
|
||||
* 规则选择器
|
||||
* eg : 输入选择语句 female {She} other {They}} ,表示'female'和'other'是两种可能的值,它们分别对应着'She'和'They'两个输出结果。
|
||||
* 如果调用select({ value: 'female' })则表示,输出 she
|
||||
*/
|
||||
class SelectFormatter {
|
||||
private readonly locale: Locale;
|
||||
private selectCache = {};
|
||||
|
||||
constructor(locale) {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
getRule(value, rules, useMemorize?) {
|
||||
if (useMemorize) {
|
||||
// 创建key,用于唯一标识
|
||||
const cacheKey = utils.generateKey<Intl.NumberFormatOptions>(this.locale, rules);
|
||||
|
||||
// 如果key存在,则使用缓存中的替代
|
||||
if (this.selectCache[cacheKey]) {
|
||||
return this.selectCache[cacheKey][value] || this.selectCache[cacheKey].other;
|
||||
}
|
||||
|
||||
// 如果不存在,则进行缓存
|
||||
this.selectCache[cacheKey] = rules;
|
||||
}
|
||||
|
||||
return rules[value] || rules.other;
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectFormatter;
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import DateTimeFormatter from './fomatters/DateTimeFormatter';
|
||||
import NumberFormatter from './fomatters/NumberFormatter';
|
||||
import {DatePool, Locale, Locales, SelectPool} from '../types/types';
|
||||
import PluralFormatter from './fomatters/PluralFormatter';
|
||||
import SelectFormatter from './fomatters/SelectFormatter';
|
||||
import {FormatOptions, IntlMessageFormat} from "../types/interfaces";
|
||||
|
||||
/**
|
||||
* 默认格式化接口
|
||||
*/
|
||||
const generateFormatters = (
|
||||
locale: Locale | Locales,
|
||||
locales: Locales,
|
||||
localeConfig: Record<string, any> = { plurals: undefined },
|
||||
formatOptions: FormatOptions = {} // 自定义格式对象
|
||||
): IntlMessageFormat => {
|
||||
locale = locales || locale;
|
||||
const { plurals } = localeConfig;
|
||||
/**
|
||||
* 样式函数 ,根据格式获取格式样式, 如货币百分比, 返回相应的格式的对象,如果没有设定格式,则返回一个空对象
|
||||
* @param formatOption
|
||||
*/
|
||||
const getStyleOption = formatOption => {
|
||||
if (typeof formatOption === 'string') {
|
||||
return formatOptions[formatOption] || { option: formatOption };
|
||||
} else {
|
||||
return formatOption;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// 复数规则
|
||||
plural: (value: number, { offset = 0, ...rules }, useMemorize?) => {
|
||||
const pluralFormatter = new PluralFormatter(
|
||||
locale,
|
||||
locales,
|
||||
value - offset,
|
||||
rules[value] || rules[(plurals as any)?.(value - offset)] || rules.other,
|
||||
useMemorize
|
||||
);
|
||||
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
||||
},
|
||||
|
||||
selectordinal: (value: number, { offset = 0, ...rules }, useMemorize?) => {
|
||||
const message = rules[value] || rules[(plurals as any)?.(value - offset, true)] || rules.other;
|
||||
const pluralFormatter = new PluralFormatter(locale, locales, value - offset, message, useMemorize);
|
||||
return pluralFormatter.replaceSymbol.bind(pluralFormatter);
|
||||
},
|
||||
|
||||
// 选择规则,如果规则对象中包含与该值相对应的属性,则返回该属性的值;否则,返回 "other" 属性的值。
|
||||
select: (value: SelectPool, formatRules, useMemorize?) => {
|
||||
const selectFormatter = new SelectFormatter(locale);
|
||||
return selectFormatter.getRule(value, formatRules, useMemorize);
|
||||
},
|
||||
|
||||
// 用于将数字格式化为字符串,接受一个数字和一个格式化规则。它会根据规则返回格式化后的字符串。
|
||||
numberFormat: (value : number, formatOption, useMemorize) => {
|
||||
return new NumberFormatter(locales, getStyleOption(formatOption), useMemorize).numberFormat(value);
|
||||
},
|
||||
|
||||
// 用于将日期格式化为字符串,接受一个日期对象和一个格式化规则。它会根据规则返回格式化后的字符串。
|
||||
/**
|
||||
* eg: { year: 'numeric', month: 'long', day: 'numeric' } 是一个用于指定DateTimeFormatter如何将日期对象转换为字符串的参数。
|
||||
* \year: 'numeric' 表示年份的表示方式是数字形式(比如2023)。
|
||||
* month: 'long' 表示月份的表示方式是全名(比如January)。
|
||||
* day: 'numeric' 表示日期的表示方式是数字形式(比如1号)。
|
||||
* @param value
|
||||
* @param formatOption { year: 'numeric', month: 'long', day: 'numeric' }
|
||||
* @param useMemorize
|
||||
*/
|
||||
dateTimeFormat: (value: DatePool, formatOption, useMemorize) => {
|
||||
return new DateTimeFormatter(locales, getStyleOption(formatOption), useMemorize).dateTimeFormat(value, formatOption);
|
||||
},
|
||||
|
||||
// 用于处理未定义的值,接受一个值并直接返回它。
|
||||
undefined: value => value,
|
||||
};
|
||||
};
|
||||
|
||||
export default generateFormatters;
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import utils from '../utils/utils';
|
||||
import Translation from './Translation';
|
||||
import I18n from '../core/I18n';
|
||||
import { MessageDescriptor, MessageOptions } from '../types/interfaces';
|
||||
import { CompiledMessage } from '../types/types';
|
||||
|
||||
export function getFormatMessage(
|
||||
i18n: I18n,
|
||||
id: MessageDescriptor | string,
|
||||
values: Object | undefined = {},
|
||||
options: MessageOptions = {}
|
||||
) {
|
||||
let { message, context, formatOptions, useMemorize } = options;
|
||||
const memorize = useMemorize ?? i18n.useMemorize;
|
||||
if (typeof id !== 'string') {
|
||||
values = values || id.defaultValues;
|
||||
message = id.message || id.defaultMessage;
|
||||
context = id.context;
|
||||
id = id.id;
|
||||
}
|
||||
|
||||
// 对messages进行判空处理
|
||||
const isMissingMessage = !context && !i18n.messages[id];
|
||||
const isMissingContextMessage = context && !i18n.messages[context][id];
|
||||
const messageUnavailable = isMissingContextMessage || isMissingMessage;
|
||||
|
||||
// 对错误消息进行处理
|
||||
const messageError = i18n.error;
|
||||
if (messageError && messageUnavailable) {
|
||||
if (typeof messageError === 'function') {
|
||||
return messageError(i18n.locale, id, context);
|
||||
} else {
|
||||
return messageError;
|
||||
}
|
||||
}
|
||||
|
||||
let compliedMessage: CompiledMessage;
|
||||
if (context) {
|
||||
compliedMessage = i18n.messages[context][id] || message || id;
|
||||
} else {
|
||||
compliedMessage = i18n.messages[id] || message || id;
|
||||
}
|
||||
|
||||
// 对解析的messages进行parse解析,并输出解析后的Token
|
||||
compliedMessage = typeof compliedMessage === 'string' ? utils.compile(compliedMessage) : compliedMessage;
|
||||
|
||||
const translation = new Translation(compliedMessage, i18n.locale, i18n.locales, i18n.localeConfig, memorize);
|
||||
return translation.translate(values, formatOptions);
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import ruleUtils from '../utils/parseRuleUtils';
|
||||
import { LexerInterface } from "../types/interfaces";
|
||||
|
||||
const getMatch = ruleUtils.checkSticky()
|
||||
? // 正则表达式具有 sticky 标志
|
||||
(regexp, buffer) => regexp.exec(buffer)
|
||||
: // 正则表达式具有 global 标志,匹配的字符串长度为 0,则表示匹配失败
|
||||
(regexp, buffer) => (regexp.exec(buffer)[0].length === 0 ? null : regexp.exec(buffer));
|
||||
|
||||
class Lexer<T> implements LexerInterface<T> {
|
||||
readonly startState: string;
|
||||
readonly states: Record<string, any>;
|
||||
private buffer: string = '';
|
||||
private stack: string[] = [];
|
||||
private index;
|
||||
private line;
|
||||
private col;
|
||||
private queuedText;
|
||||
private state;
|
||||
private groups;
|
||||
private error;
|
||||
private regexp;
|
||||
private fast;
|
||||
private queuedGroup;
|
||||
private value;
|
||||
|
||||
constructor(states, state) {
|
||||
this.startState = state;
|
||||
this.states = states;
|
||||
this.buffer = '';
|
||||
this.stack = [];
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset(data?, info?) {
|
||||
this.buffer = data || '';
|
||||
this.index = 0;
|
||||
this.line = info ? info.line : 1;
|
||||
this.col = info ? info.col : 1;
|
||||
this.queuedText = info ? info.queuedText : '';
|
||||
this.setState(info ? info.state : this.startState);
|
||||
this.stack = info && info.stack ? info.stack.slice() : [];
|
||||
return this;
|
||||
}
|
||||
|
||||
private setState(state) {
|
||||
if (!state || this.state === state) {
|
||||
return;
|
||||
}
|
||||
this.state = state;
|
||||
const info = this.states[state];
|
||||
this.groups = info.groups;
|
||||
this.error = info.error;
|
||||
this.regexp = info.regexp;
|
||||
this.fast = info.fast;
|
||||
}
|
||||
|
||||
private popState() {
|
||||
this.setState(this.stack.pop());
|
||||
}
|
||||
|
||||
private pushState(state) {
|
||||
this.stack.push(this.state);
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
private getGroup(match) {
|
||||
const groupCount = this.groups.length;
|
||||
for (let i = 0; i < groupCount; i++) {
|
||||
if (match[i + 1] !== undefined) {
|
||||
return this.groups[i];
|
||||
}
|
||||
}
|
||||
throw new Error('No token type found matching text!');
|
||||
}
|
||||
|
||||
private tokenToString() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
// 迭代获取下一个 token
|
||||
public next() {
|
||||
const index = this.index;
|
||||
|
||||
if (this.queuedGroup) {
|
||||
const token = this.getToken(this.queuedGroup, this.queuedText, index);
|
||||
this.queuedGroup = null;
|
||||
this.queuedText = '';
|
||||
return token;
|
||||
}
|
||||
|
||||
const buffer = this.buffer;
|
||||
if (index === buffer.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fastGroup = this.fast[buffer.charCodeAt(index)];
|
||||
if (fastGroup) {
|
||||
return this.getToken(fastGroup, buffer.charAt(index), index);
|
||||
}
|
||||
|
||||
// 如果没有快速匹配,那么使用预先编译的正则表达式进行匹配操作
|
||||
const regexp = this.regexp;
|
||||
regexp.lastIndex = index;
|
||||
const match = getMatch(regexp, buffer);
|
||||
|
||||
const error = this.error;
|
||||
if (match == null) {
|
||||
return this.getToken(error, buffer.slice(index, buffer.length), index);
|
||||
}
|
||||
|
||||
const group = this.getGroup(match);
|
||||
const text = match[0];
|
||||
|
||||
if (error.fallback && match.index !== index) {
|
||||
this.queuedGroup = group;
|
||||
this.queuedText = text;
|
||||
return this.getToken(error, buffer.slice(index, match.index), index);
|
||||
}
|
||||
|
||||
return this.getToken(group, text, index);
|
||||
}
|
||||
|
||||
private getToken(group, text, offset) {
|
||||
let lineNum = 0;
|
||||
let last = 1; // 最后一个换行符的索引位置
|
||||
if (group.lineBreaks) {
|
||||
const matchNL = /\n/g;
|
||||
if (text === '\n') {
|
||||
lineNum = 1;
|
||||
} else {
|
||||
while (matchNL.exec(text)) {
|
||||
lineNum++;
|
||||
last = matchNL.lastIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const token = {
|
||||
type: (typeof group.type === 'function' && group.type(text)) || group.defaultType,
|
||||
value: typeof group.value === 'function' ? group.value(text) : text,
|
||||
text: text,
|
||||
toString: this.tokenToString,
|
||||
offset: offset, // 标记在输入 buffer 中的偏移量
|
||||
lineBreaks: lineNum,
|
||||
line: this.line, // token 所在的行号
|
||||
col: this.col, // token 所在的列号
|
||||
};
|
||||
|
||||
const size = text.length;
|
||||
this.index += size;
|
||||
this.line += lineNum;
|
||||
if (lineNum !== 0) {
|
||||
this.col = size - last + 1;
|
||||
} else {
|
||||
this.col += size;
|
||||
}
|
||||
|
||||
if (group.shouldThrow) {
|
||||
throw new Error('Invalid Syntax!');
|
||||
}
|
||||
|
||||
if (group.pop) {
|
||||
this.popState();
|
||||
} else if (group.push) {
|
||||
this.pushState(group.push);
|
||||
} else if (group.next) {
|
||||
this.setState(group.next);
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
// 增加迭代器
|
||||
[Symbol.iterator]() {
|
||||
return {
|
||||
next: (): IteratorResult<T> => {
|
||||
const token = this.next();
|
||||
return { value: token, done: !token } as IteratorResult<T>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Lexer;
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
const body: Record<string, any> = {
|
||||
doubleapos: { match: "''", value: () => "'" },
|
||||
quoted: {
|
||||
lineBreaks: true,
|
||||
match: /'[{}#](?:[^]*?[^'])?'(?!')/u,
|
||||
value: src => src.slice(1, -1).replace(/''/g, "'"),
|
||||
},
|
||||
argument: {
|
||||
lineBreaks: true,
|
||||
match: /\{\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u,
|
||||
push: 'arg',
|
||||
value: src => src.substring(1).trim(),
|
||||
},
|
||||
octothorpe: '#',
|
||||
end: { match: '}', pop: 1 },
|
||||
content: { lineBreaks: true, match: /[^][^{}#']*/u },
|
||||
};
|
||||
|
||||
const arg: Record<string, any> = {
|
||||
select: {
|
||||
lineBreaks: true,
|
||||
match: /,\s*(?:plural|select|selectordinal)\s*,\s*/u,
|
||||
next: 'select',
|
||||
value: src => src.split(',')[1].trim(),
|
||||
},
|
||||
'func-args': {
|
||||
lineBreaks: true,
|
||||
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*,/u,
|
||||
next: 'body',
|
||||
value: src => src.split(',')[1].trim(),
|
||||
},
|
||||
'func-simple': {
|
||||
lineBreaks: true,
|
||||
match: /,\s*[^\p{Pat_Syn}\p{Pat_WS}]+\s*/u,
|
||||
value: src => src.substring(1).trim(),
|
||||
},
|
||||
end: { match: '}', pop: 1 },
|
||||
};
|
||||
|
||||
const select: Record<string, any> = {
|
||||
offset: {
|
||||
lineBreaks: true,
|
||||
match: /\s*offset\s*:\s*\d+\s*/u,
|
||||
value: src => src.split(':')[1].trim(),
|
||||
},
|
||||
case: {
|
||||
lineBreaks: true,
|
||||
match: /\s*(?:=\d+|[^\p{Pat_Syn}\p{Pat_WS}]+)\s*\{/u,
|
||||
push: 'body',
|
||||
value: src => src.substring(0, src.indexOf('{')).trim(),
|
||||
},
|
||||
end: { match: /\s*\}/u, pop: 1 },
|
||||
};
|
||||
|
||||
export const mappingRule: Record<string, any> = {
|
||||
body,
|
||||
arg,
|
||||
select
|
||||
};
|
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import Lexer from './Lexer';
|
||||
import { mappingRule } from './mappingRule';
|
||||
import ruleUtils from '../utils/parseRuleUtils';
|
||||
import { RawToken } from "../types/types";
|
||||
|
||||
const defaultErrorRule = ruleUtils.getRuleOptions('error', { lineBreaks: true, shouldThrow: true });
|
||||
|
||||
// 解析规则并生成词法分析器所需的数据结构,以便进行词法分析操作
|
||||
function parseRules(rules: Record<string, any>, hasStates: boolean): Record<string, any> {
|
||||
let errorRule: Record<string, any> | null = null;
|
||||
const fast = {};
|
||||
let enableFast = true;
|
||||
let unicodeFlag = null;
|
||||
const groups: Record<string, any>[] = [];
|
||||
const parts: string[] = [];
|
||||
|
||||
// 检查是否存在 fallback 规则,若存在则禁用快速匹配
|
||||
enableFast = isExistsFallback(rules, enableFast);
|
||||
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const options = rules[i];
|
||||
if (options.include) {
|
||||
throw new Error('Inheritance is not allowed in stateless lexers!');
|
||||
}
|
||||
|
||||
errorRule = isOptionsErrorOrFallback(options, errorRule);
|
||||
|
||||
const match = options.match.slice();
|
||||
if (enableFast) {
|
||||
// 如果快速匹配允许,则将单字符的规则存入 fast 对象
|
||||
processFast(match, fast, options);
|
||||
}
|
||||
|
||||
// 检查规则中是否存在不适当的状态切换选项
|
||||
if (options.pop || options.push || options.next) {
|
||||
checkStateOptions(hasStates, options);
|
||||
}
|
||||
// 只有具有 .match 的规则才会被包含在正则表达式中
|
||||
if (match.length === 0) {
|
||||
continue;
|
||||
}
|
||||
enableFast = false;
|
||||
|
||||
groups.push(options);
|
||||
|
||||
// 检查是否所有规则都使用了 unicode 标志,或者都未使用
|
||||
unicodeFlag = checkUnicode(match, unicodeFlag, options);
|
||||
|
||||
const pat = ruleUtils.getRegUnion(match.map(ruleUtils.getReg));
|
||||
const regexp = new RegExp(pat);
|
||||
if (regexp.test('')) {
|
||||
throw new Error('The regex matched the empty string!');
|
||||
}
|
||||
const groupCount = ruleUtils.getRegGroups(pat);
|
||||
if (groupCount > 0) {
|
||||
throw new Error('The regular expression uses capture groups, use (?: … ) instead!');
|
||||
}
|
||||
|
||||
// 检测规则是否匹配换行符
|
||||
if (!options.lineBreaks && regexp.test('\n')) {
|
||||
throw new Error('The matching rule must contain lineBreaks.');
|
||||
}
|
||||
|
||||
parts.push(ruleUtils.getRegCapture(pat));
|
||||
}
|
||||
|
||||
// 如果没有 fallback 规则,则使用 sticky 标志,只在当前索引位置寻找匹配项,如果不支持 sticky 标志,则使用无法被否定的空模式来模拟
|
||||
const fallbackRule = errorRule && errorRule.fallback;
|
||||
let flags = ruleUtils.checkSticky() && !fallbackRule ? 'ym' : 'gm';
|
||||
const suffix = ruleUtils.checkSticky() || fallbackRule ? '' : '|';
|
||||
|
||||
if (unicodeFlag === true) {
|
||||
flags += 'u';
|
||||
}
|
||||
const combined = new RegExp(ruleUtils.getRegUnion(parts) + suffix, flags);
|
||||
|
||||
return {
|
||||
regexp: combined,
|
||||
groups: groups,
|
||||
fast: fast,
|
||||
error: errorRule || defaultErrorRule,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkStateGroup(group: Record<string, any>, name: string, map: Record<string, any>) {
|
||||
const state = group && (group.push || group.next);
|
||||
if (state && !map[state]) {
|
||||
throw new Error('The state is missing.');
|
||||
}
|
||||
if (group && group.pop && +group.pop !== 1) {
|
||||
throw new Error('The value of pop must be 1.');
|
||||
}
|
||||
}
|
||||
|
||||
// 将国际化解析规则注入分词器中
|
||||
function parseMappingRule(mappingRule: Record<string, any>, start?: string): Lexer<RawToken> {
|
||||
const keys = Object.getOwnPropertyNames(mappingRule);
|
||||
|
||||
if (!start) {
|
||||
start = keys[0];
|
||||
}
|
||||
|
||||
// 将每个状态的规则解析为规则数组,并存储在 ruleMap 对象中
|
||||
const ruleMap = keys.reduce((map, key) => {
|
||||
map[key] = ruleUtils.getRules(mappingRule[key]);
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
// 处理规则中的 include 声明,将被包含的规则添加到相应的状态中
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const rules = ruleMap[key];
|
||||
const included = {};
|
||||
|
||||
for (let j = 0; j < rules.length; j++) {
|
||||
const rule = rules[j];
|
||||
if (!rule.include) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const splice = [j, 1];
|
||||
if (rule.include !== key && !included[rule.include]) {
|
||||
included[rule.include] = true;
|
||||
const newRules = ruleMap[rule.include];
|
||||
|
||||
if (!newRules) {
|
||||
throw new Error('Cannot contain a state that does not exist!');
|
||||
}
|
||||
|
||||
newRules.forEach(newRule => {
|
||||
if (!rules.includes(newRule)) {
|
||||
splice.push(newRule);
|
||||
}
|
||||
});
|
||||
}
|
||||
rules.splice.apply(rules, splice);
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
const map = {};
|
||||
|
||||
// 将规则映射为词法分析器数据结构,并存储在 map 对象中
|
||||
keys.forEach(key => {
|
||||
map[key] = parseRules(ruleMap[key], true);
|
||||
});
|
||||
|
||||
// 检查状态组中的规则是否正确引用了其他状态
|
||||
keys.forEach(name => {
|
||||
const state = map[name];
|
||||
const groups = state.groups;
|
||||
groups.forEach(group => {
|
||||
checkStateGroup(group, name, map);
|
||||
});
|
||||
const fastKeys = Object.getOwnPropertyNames(state.fast);
|
||||
fastKeys.forEach(fastKey => {
|
||||
checkStateGroup(state.fast[fastKey], name, map);
|
||||
});
|
||||
});
|
||||
|
||||
return new Lexer(map, start);
|
||||
}
|
||||
|
||||
function processFast(match, fast: {}, options) {
|
||||
while (match.length && typeof match[0] === 'string' && match[0].length === 1) {
|
||||
const word = match.shift();
|
||||
fast[word.charCodeAt(0)] = options;
|
||||
}
|
||||
}
|
||||
|
||||
function handleErrorRule(options, errorRule: Record<string, any>) {
|
||||
if (!options.fallback === !errorRule.fallback) {
|
||||
throw new Error('errorRule can only set one!');
|
||||
} else {
|
||||
throw new Error('fallback and error cannot be set at the same time!');
|
||||
}
|
||||
}
|
||||
|
||||
function checkUnicode(match, unicodeFlag, options) {
|
||||
for (let j = 0; j < match.length; j++) {
|
||||
const obj = match[j];
|
||||
if (!ruleUtils.checkRegExp(obj)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (unicodeFlag === null) {
|
||||
unicodeFlag = obj.unicode;
|
||||
} else if (unicodeFlag !== obj.unicode && options.fallback === false) {
|
||||
throw new Error('If the /u flag is used, all!');
|
||||
}
|
||||
}
|
||||
return unicodeFlag;
|
||||
}
|
||||
|
||||
function checkStateOptions(hasStates: boolean, options) {
|
||||
if (!hasStates) {
|
||||
throw new Error('State toggle options are not allowed in stateless tokenizers!');
|
||||
}
|
||||
if (options.fallback) {
|
||||
throw new Error('State toggle options are not allowed on fallback tokens!');
|
||||
}
|
||||
}
|
||||
|
||||
function isExistsFallback(rules: Record<string, any>, enableFast: boolean) {
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
if (rules[i].fallback) {
|
||||
enableFast = false;
|
||||
}
|
||||
}
|
||||
return enableFast;
|
||||
}
|
||||
|
||||
function isOptionsErrorOrFallback(options, errorRule: Record<string, any> | null) {
|
||||
if (options.error || options.fallback) {
|
||||
// 只能设置一个 errorRule
|
||||
if (errorRule) {
|
||||
handleErrorRule(options, errorRule);
|
||||
}
|
||||
errorRule = options;
|
||||
}
|
||||
return errorRule;
|
||||
}
|
||||
|
||||
export const lexer = parseMappingRule(mappingRule);
|
||||
|
||||
export default parseMappingRule;
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import { lexer } from './parseMappingRule';
|
||||
import { RawToken, Token } from '../types/types';
|
||||
import { DEFAULT_PLURAL_KEYS } from '../constants';
|
||||
import { Content, FunctionArg, PlainArg, Select, TokenContext } from '../types/interfaces';
|
||||
import Lexer from "./Lexer";
|
||||
|
||||
const getContext = (lt: Record<string, any>): TokenContext => ({
|
||||
offset: lt.offset,
|
||||
line: lt.line,
|
||||
col: lt.col,
|
||||
text: lt.text,
|
||||
lineNum: lt.lineBreaks,
|
||||
});
|
||||
|
||||
export const checkSelectType = (value: string): boolean => {
|
||||
return value === 'plural' || value === 'select' || value === 'selectordinal';
|
||||
}
|
||||
|
||||
class Parser {
|
||||
lexer: Lexer<RawToken>;
|
||||
cardinalKeys: string[] = DEFAULT_PLURAL_KEYS;
|
||||
ordinalKeys: string[] = DEFAULT_PLURAL_KEYS;
|
||||
|
||||
constructor(message: string) {
|
||||
this.lexer = lexer.reset(message);
|
||||
}
|
||||
|
||||
isSelectKeyValid(token: RawToken, type: Select['type'], value: string) {
|
||||
if (value[0] === '=') {
|
||||
if (type === 'select') {
|
||||
throw new Error(`The key value of the select type is invalid.`);
|
||||
}
|
||||
} else if (type !== 'select') {
|
||||
const values = type === 'plural' ? this.cardinalKeys : this.ordinalKeys;
|
||||
if (values.length > 0 && !values.includes(value)) {
|
||||
throw new Error(`${type} type key value is invalid.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processSelect({ value: arg }: any, isPlural: boolean, context: TokenContext, type: Select['type']): Select {
|
||||
const select: Select = { type, arg, cases: [], ctx: context };
|
||||
|
||||
if (type === 'plural' || type === 'selectordinal') {
|
||||
isPlural = true;
|
||||
}
|
||||
|
||||
for (const token of this.lexer) {
|
||||
switch (token.type) {
|
||||
case 'offset': {
|
||||
if (type === 'select') {
|
||||
throw new Error('The complex offset of the select type is incorrect.');
|
||||
}
|
||||
if (select.cases.length > 0) {
|
||||
throw new Error('The complex offset must be set before cases.');
|
||||
}
|
||||
|
||||
select.offset = Number(token.value);
|
||||
context.text += token.text;
|
||||
context.lineNum += token.lineBreaks;
|
||||
break;
|
||||
}
|
||||
case 'case': {
|
||||
this.isSelectKeyValid(token, type, token.value);
|
||||
select.cases.push({
|
||||
key: token.value.replace(/=/g, ''),
|
||||
tokens: this.parse(isPlural),
|
||||
ctx: getContext(token),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'end': {
|
||||
return select;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unrecognized analyzer token: ${token.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new Error('The message end position is invalid.');
|
||||
}
|
||||
|
||||
parseToken(token: RawToken, isPlural: boolean): PlainArg | FunctionArg | Select {
|
||||
const context = getContext(token);
|
||||
const nextToken = this.lexer.next();
|
||||
|
||||
if (!nextToken) {
|
||||
throw new Error('The message end position is invalid.');
|
||||
}
|
||||
|
||||
context.text += nextToken.text;
|
||||
context.lineNum += nextToken.lineBreaks;
|
||||
|
||||
switch (nextToken.type) {
|
||||
case 'end': {
|
||||
return { type: 'argument', arg: token.value, ctx: context };
|
||||
}
|
||||
case 'func-simple': {
|
||||
const end = this.lexer.next();
|
||||
if (!end) {
|
||||
throw new Error('The message end position is invalid.');
|
||||
}
|
||||
if (end.type !== 'end') {
|
||||
throw new Error(`Unrecognized analyzer token: ${end.type}`);
|
||||
}
|
||||
context.text += end.text;
|
||||
if (checkSelectType(nextToken.value.toLowerCase())) {
|
||||
throw new Error(`Invalid parameter type: ${nextToken.value}`);
|
||||
}
|
||||
return {
|
||||
type: 'function',
|
||||
arg: token.value,
|
||||
key: nextToken.value,
|
||||
ctx: context,
|
||||
};
|
||||
}
|
||||
case 'func-args': {
|
||||
if (checkSelectType(nextToken.value.toLowerCase())) {
|
||||
throw new Error(`Invalid parameter type: ${nextToken.value}`);
|
||||
}
|
||||
let param = this.parse(isPlural);
|
||||
|
||||
return {
|
||||
type: 'function',
|
||||
arg: token.value,
|
||||
key: nextToken.value,
|
||||
param,
|
||||
ctx: context,
|
||||
};
|
||||
}
|
||||
case 'select':
|
||||
if (checkSelectType(nextToken.value)) {
|
||||
return this.processSelect(token, isPlural, context, nextToken.value);
|
||||
} else {
|
||||
throw new Error(`Invalid select type: ${nextToken.value}`);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unrecognized analyzer token: ${nextToken.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 在根级别解析时,遇到结束符号即结束解析并返回结果;而在非根级别解析时,遇到结束符号会被视为不合法的结束位置,抛出错误
|
||||
parse(isPlural: boolean, isRoot?: boolean): Array<Content | PlainArg | FunctionArg | Select> {
|
||||
const tokens: any[] = [];
|
||||
let content: string | Content | null = null;
|
||||
|
||||
for (const token of this.lexer) {
|
||||
if (token.type === 'argument') {
|
||||
if (content) {
|
||||
content = null;
|
||||
}
|
||||
tokens.push(this.parseToken(token, isPlural));
|
||||
} else if (token.type === 'octothorpe' && isPlural) {
|
||||
if (content) {
|
||||
content = null;
|
||||
}
|
||||
tokens.push({ type: 'octothorpe' });
|
||||
} else if (token.type === 'end' && !isRoot) {
|
||||
return tokens;
|
||||
} else if (token.type === 'doubleapos') {
|
||||
tokens.push(token.value);
|
||||
} else if (token.type === 'quoted') {
|
||||
tokens.push(token.value)
|
||||
} else if (token.type === 'content') {
|
||||
tokens.push(token.value);
|
||||
} else {
|
||||
let value = token.value;
|
||||
if (!isPlural && token.type === 'quoted' && value[0] === '#') {
|
||||
if (value.includes('{')) {
|
||||
throw new Error(`Invalid template: ${value}`);
|
||||
}
|
||||
value = token.text;
|
||||
}
|
||||
if (content) {
|
||||
content = value;
|
||||
} else {
|
||||
content = value;
|
||||
tokens.push(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isRoot) {
|
||||
return tokens;
|
||||
}
|
||||
throw new Error('The message end position is invalid.');
|
||||
}
|
||||
}
|
||||
|
||||
export default function parse(message: string): Array<Content | PlainArg | FunctionArg | Select> {
|
||||
const parser = new Parser(message);
|
||||
return parser.parse(false, true);
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import {
|
||||
AllLocaleConfig,
|
||||
AllMessages,
|
||||
Locale,
|
||||
Locales,
|
||||
Error,
|
||||
PluralCategory,
|
||||
DatePool,
|
||||
SelectPool,
|
||||
RawToken,
|
||||
} from './types';
|
||||
import I18n from '../core/I18n';
|
||||
import Lexer from '../parser/Lexer';
|
||||
import injectI18n from "../core/components/InjectI18n";
|
||||
|
||||
// FormattedMessage的参数定义
|
||||
export interface FormattedMessageProps extends MessageDescriptor {
|
||||
values?: object;
|
||||
tagName?: string;
|
||||
|
||||
children?(nodes: any[]): any;
|
||||
}
|
||||
|
||||
// 信息描述对象,有id、信息、内容
|
||||
export interface MessageDescriptor extends MessageOptions {
|
||||
id: string;
|
||||
defaultMessage?: string;
|
||||
defaultValues?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface MessageOptions {
|
||||
comment?: string;
|
||||
message?: string;
|
||||
context?: string;
|
||||
formatOptions?: FormatOptions;
|
||||
useMemorize?: boolean;
|
||||
}
|
||||
|
||||
// I18n类的缓存定义
|
||||
export interface I18nCache {
|
||||
dateTimeFormat: Record<string, Intl.DateTimeFormat>;
|
||||
numberFormat: Record<string, Intl.NumberFormat>;
|
||||
plurals: Record<string, Intl.PluralRules>;
|
||||
messages: Record<string, IntlMessageFormat>;
|
||||
select: Record<string, object>;
|
||||
octothorpe: Record<string, any>;
|
||||
}
|
||||
|
||||
// I18n类的传参
|
||||
export interface I18nProps {
|
||||
locale?: Locale;
|
||||
locales?: Locales;
|
||||
messages?: AllMessages;
|
||||
localeConfig?: AllLocaleConfig;
|
||||
useMemorize?: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
// 消息格式化选项类型
|
||||
export interface FormatOptions {
|
||||
dateTimeFormat?: Intl.DateTimeFormatOptions;
|
||||
numberFormat?: Intl.NumberFormatOptions;
|
||||
plurals?: Intl.PluralRulesOptions;
|
||||
}
|
||||
|
||||
export interface InjectOptions {
|
||||
isUsingForwardRef?: boolean;
|
||||
ensureContext?: boolean;
|
||||
}
|
||||
|
||||
export interface I18nProviderProps {
|
||||
i18n?: I18n;
|
||||
locale?: Locale;
|
||||
messages?: AllMessages;
|
||||
defaultLocale?: string;
|
||||
RenderOnLocaleChange?: boolean;
|
||||
children?: any;
|
||||
uesMemorize?: boolean;
|
||||
}
|
||||
|
||||
export interface IntlMessageFormat extends I18nProviderProps, MessageOptions {
|
||||
plural: (
|
||||
value: number,
|
||||
{ offset, ...rules }: { [x: string]: any; offset?: number },
|
||||
useMemorize?: boolean
|
||||
) => (ctx: any) => any[];
|
||||
selectordinal: (
|
||||
value: number,
|
||||
{ offset, ...rules }: { [x: string]: any; offset?: number },
|
||||
useMemorize?: boolean
|
||||
) => (ctx: any) => any[];
|
||||
select: (value: SelectPool, formatRules: any, useMemorize?: boolean) => any;
|
||||
numberFormat: (value: number, formatOption: any, useMemorize: boolean) => string;
|
||||
dateTimeFormat: (value: DatePool, formatOption: any, useMemorize: boolean) => string;
|
||||
undefined: (value: any) => any;
|
||||
}
|
||||
|
||||
//错误信息的事件
|
||||
export interface MissingMessageEvent {
|
||||
locale: Locale;
|
||||
id: string;
|
||||
context?: string;
|
||||
}
|
||||
|
||||
export interface LexerInterface<T> {
|
||||
reset: (data?: string, info?: Record<string, any>) => Lexer<T>;
|
||||
next: () => RawToken | undefined;
|
||||
|
||||
[Symbol.iterator](): Iterator<T>;
|
||||
}
|
||||
|
||||
export interface TokenContext {
|
||||
// token 索引值的偏移量
|
||||
offset: number;
|
||||
|
||||
// token 开始计算的初始行号
|
||||
line: number;
|
||||
|
||||
// token 开始计算的初始列号
|
||||
col: number;
|
||||
|
||||
// 原始输入
|
||||
text: string;
|
||||
|
||||
// 换行数
|
||||
lineNum: number;
|
||||
}
|
||||
|
||||
export interface Content {
|
||||
type: 'content';
|
||||
value: string;
|
||||
ctx: TokenContext;
|
||||
}
|
||||
|
||||
// 需要解析参数定义
|
||||
export interface PlainArg {
|
||||
type: 'argument';
|
||||
arg: string;
|
||||
ctx: TokenContext;
|
||||
}
|
||||
|
||||
export interface Octothorpe {
|
||||
type: 'octothorpe';
|
||||
ctx: TokenContext;
|
||||
}
|
||||
|
||||
export interface FunctionArg {
|
||||
type: 'function';
|
||||
arg: string;
|
||||
key: string;
|
||||
param?: Array<Content | PlainArg | FunctionArg | Select | Octothorpe>;
|
||||
ctx: TokenContext;
|
||||
}
|
||||
|
||||
export interface SelectCase {
|
||||
key: string;
|
||||
tokens: Array<Content | PlainArg | FunctionArg | Select | Octothorpe>;
|
||||
ctx: TokenContext;
|
||||
}
|
||||
|
||||
// 选择模式
|
||||
export interface Select {
|
||||
type: 'plural' | 'select' | 'selectordinal';
|
||||
arg: string;
|
||||
cases: SelectCase[];
|
||||
offset?: number;
|
||||
ctx: TokenContext;
|
||||
}
|
||||
|
||||
export interface InjectedIntl {
|
||||
|
||||
// 日期格式化
|
||||
formatDate(value: DatePool, options?: Intl.DateTimeFormatOptions): string;
|
||||
|
||||
// 时间格式化
|
||||
formatTime(value: DatePool, options?: Intl.DateTimeFormatOptions): string;
|
||||
|
||||
// 数字格式化
|
||||
formatNumber(value: number, options?: Intl.NumberFormatOptions): string;
|
||||
|
||||
// 信息格式化
|
||||
formatMessage(
|
||||
messageDescriptor: MessageDescriptor,
|
||||
values?: object,
|
||||
options?: MessageOptions,
|
||||
useMemorize?: boolean
|
||||
): string;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import { Content, MissingMessageEvent, Octothorpe, PlainArg, Select, FunctionArg } from './interfaces';
|
||||
|
||||
export type Error = string | ((message, id, context) => string);
|
||||
|
||||
export type Locale = string;
|
||||
|
||||
export type Locales = Locale | Locale[];
|
||||
|
||||
export type LocaleConfig = { plurals?: Function };
|
||||
|
||||
export type AllLocaleConfig = Record<Locale, LocaleConfig>;
|
||||
|
||||
type CompiledMessagePart = string | Array<string | Array<string | (string | undefined)> | Record<string, unknown>>;
|
||||
|
||||
export type CompiledMessage = string | CompiledMessagePart[];
|
||||
|
||||
export type Messages = Record<string, string> | Record<string, CompiledMessage>;
|
||||
|
||||
export type AllMessages = Record<string, string> | Record<Locale, Messages>;
|
||||
|
||||
export type EventCallback = (...args: any[]) => any;
|
||||
|
||||
// 资源事件
|
||||
export type Events = {
|
||||
change: () => void;
|
||||
missing: (event: MissingMessageEvent) => void;
|
||||
};
|
||||
|
||||
// 默认复数规则
|
||||
export type PluralCategory = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
||||
|
||||
export type Token = Content | PlainArg | FunctionArg | Select | Octothorpe;
|
||||
|
||||
export type DatePool = Date | string;
|
||||
|
||||
export type SelectPool = string | object;
|
||||
|
||||
export type RawToken = {
|
||||
type: string;
|
||||
value: string;
|
||||
text: string;
|
||||
toString: () => string;
|
||||
offset: number;
|
||||
lineBreaks: number;
|
||||
line: number;
|
||||
col: number;
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import { isMemo, ForwardRef } from '@cloudsop/horizon';
|
||||
import {
|
||||
HORIZON_FORWARD_REF_STATICS,
|
||||
HORIZON_MEMO_STATICS,
|
||||
HORIZON_STATICS,
|
||||
NATIVE_STATICS,
|
||||
} from '../constants';
|
||||
|
||||
const staticsMap = new Map();
|
||||
staticsMap.set(ForwardRef, HORIZON_FORWARD_REF_STATICS);
|
||||
|
||||
// 确定给定的组件是否为Memo组件,并返回相应的静态属性
|
||||
function getStatics(component) {
|
||||
if (isMemo(component)) {
|
||||
return HORIZON_MEMO_STATICS;
|
||||
}
|
||||
|
||||
if (staticsMap.has(component['vtype'])) {
|
||||
return staticsMap.get(component['vtype']) || HORIZON_STATICS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定的对象属性描述是否有效
|
||||
* @param sourceComponent
|
||||
* @param key
|
||||
*/
|
||||
function isDescriptorValid<U>(sourceComponent: U, key: string | symbol) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(sourceComponent, key);
|
||||
return descriptor && (!descriptor.get || descriptor.get.prototype);
|
||||
}
|
||||
|
||||
// 将一个对象的非react静态属性复制到另一个对象上,并返回马目标对象
|
||||
function copyStaticProps<T, U>(targetComponent: T, sourceComponent: U): T {
|
||||
if (typeof sourceComponent === 'string') {
|
||||
return targetComponent;
|
||||
}
|
||||
// 递归拷贝静态属性
|
||||
const inheritedComponent = Object.getPrototypeOf(sourceComponent);
|
||||
if (inheritedComponent && inheritedComponent !== Object.prototype) {
|
||||
copyStaticProps(targetComponent, inheritedComponent);
|
||||
}
|
||||
|
||||
// 获取源组件的属性列表
|
||||
const keys: (string | symbol)[] = [
|
||||
//获取指定对象自身的所有属性的名称(包括不可枚举属性)
|
||||
...Object.getOwnPropertyNames(sourceComponent),
|
||||
|
||||
//获取指定对象自身的所有 Symbol 类型的属性的名称(包括不可枚举属性)
|
||||
...Object.getOwnPropertySymbols(sourceComponent),
|
||||
];
|
||||
|
||||
// 获取目标组件和源组件的静态属性
|
||||
const targetStatics = getStatics(targetComponent);
|
||||
const sourceStatics = getStatics(sourceComponent);
|
||||
|
||||
keys.forEach(key => {
|
||||
if (
|
||||
!NATIVE_STATICS[key] &&
|
||||
!(targetStatics && targetStatics[key]) &&
|
||||
!(sourceStatics && sourceStatics[key]) &&
|
||||
isDescriptorValid(sourceComponent, key)
|
||||
) {
|
||||
try {
|
||||
// 在一个已有的targetComponent对象上增加sourceComponent的属性
|
||||
Object.defineProperty(targetComponent, key, Object.getOwnPropertyDescriptor(sourceComponent, key)!);
|
||||
} catch (e) {}
|
||||
}
|
||||
});
|
||||
|
||||
return targetComponent;
|
||||
};
|
||||
|
||||
export default copyStaticProps;
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import { EventCallback } from "../types/types";
|
||||
|
||||
/**
|
||||
* 定义一个时间触发器类,使用泛型实现动态时间的监听
|
||||
*/
|
||||
class EventDispatcher<E extends Record<string, EventCallback>> {
|
||||
// 声明_events,用于存储事件和对应的监听器
|
||||
private _events: Map<keyof E, Set<EventCallback>>;
|
||||
|
||||
constructor() {
|
||||
this._events = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* on 方法,向指定的事件添加监听器,并返回一个用于移除该监听器的函数
|
||||
* @param event
|
||||
* @param listener
|
||||
*/
|
||||
on(event: keyof E, listener: E[keyof E]): () => void {
|
||||
if (!this._events.has(event)) {
|
||||
this._events.set(event, new Set());
|
||||
}
|
||||
const listeners = this._events.get(event)!;
|
||||
listeners.add(listener);
|
||||
|
||||
return () => {
|
||||
this.removeListener(event, listener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* removeListener 方法,移除指定事件的监听器
|
||||
* @param event
|
||||
* @param listener
|
||||
*/
|
||||
removeListener(event: keyof E, listener: E[keyof E]): void {
|
||||
if (!this._events.has(event)) {
|
||||
return;
|
||||
}
|
||||
const listeners = this._events.get(event)!;
|
||||
listeners.delete(listener);
|
||||
if (listeners.size === 0) {
|
||||
this._events.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* emit 方法,触发指定事件,并按照监听器注册顺序执行监听器
|
||||
* @param event
|
||||
* @param args
|
||||
*/
|
||||
emit(event: keyof E, ...args: Parameters<E[keyof E]>): void {
|
||||
if (!this._events.has(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取该事件对应的监听器集合,并按照注册顺序执行每个监听器
|
||||
const listeners = this._events.get(event)!;
|
||||
for (const listener of listeners) {
|
||||
listener.apply(this, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EventDispatcher;
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* 将parse后的Token数组针对不同的匀速类型进行处理
|
||||
*/
|
||||
enum TokenType {
|
||||
octothorpe = 'OCTOTHORPE',
|
||||
argument = 'ARGUMENT',
|
||||
function = 'FUNCTION',
|
||||
}
|
||||
|
||||
const processToken = token => {
|
||||
if (typeof token === 'string') {
|
||||
return token;
|
||||
} else if (TokenType[token.type] === 'OCTOTHORPE') { // token为符号
|
||||
return '#';
|
||||
} else if (TokenType[token.type] === 'ARGUMENT') { // token为变量
|
||||
return [token.arg];
|
||||
} else if (TokenType[token.type] === 'FUNCTION') { // token为函数方法
|
||||
const _param = token.param && token.param.tokens[0];
|
||||
const param = typeof _param === 'string' ? _param.trim() : _param;
|
||||
return [token.arg, token.key, param].filter(Boolean);
|
||||
}
|
||||
|
||||
const offset = token.offset ? parseInt(token.offset) : undefined;
|
||||
|
||||
const tempFormatProps = {};
|
||||
token.cases.forEach(item => {
|
||||
tempFormatProps[item.key] = getTokenAST(item.tokens);
|
||||
});
|
||||
|
||||
const mergedProps = Object.assign({}, { offset }, tempFormatProps);
|
||||
|
||||
return [token.arg, token.type, mergedProps];
|
||||
};
|
||||
|
||||
function getTokenAST(tokens) {
|
||||
if (!Array.isArray(tokens)) {
|
||||
return tokens.join('');
|
||||
}
|
||||
return tokens.map(token => processToken(token));
|
||||
}
|
||||
|
||||
export default getTokenAST;
|
|
@ -0,0 +1,207 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
function getType(input: any): string {
|
||||
const str: string = Object.prototype.toString.call(input);
|
||||
return str.slice(8, -1).toLowerCase();
|
||||
}
|
||||
|
||||
const createTypeChecker = (type: string) => {
|
||||
return (input: any) => {
|
||||
return getType(input) === type.toLowerCase();
|
||||
};
|
||||
};
|
||||
|
||||
const checkObject = (input: any) => input !== null && typeof input === 'object';
|
||||
|
||||
const checkRegExp = createTypeChecker('RegExp');
|
||||
|
||||
const checkSticky = () => typeof new RegExp('')?.sticky === 'boolean';
|
||||
|
||||
// 转义正则表达式中的特殊字符
|
||||
function transferReg(s: string): string {
|
||||
return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
}
|
||||
|
||||
// 计算正则表达式中捕获组的数量
|
||||
function getRegGroups(s: string): number {
|
||||
const re = new RegExp('|' + s);
|
||||
return re.exec('')?.length! - 1;
|
||||
}
|
||||
|
||||
// 创建一个捕获组的正则表达式模式
|
||||
function getRegCapture(s: string): string {
|
||||
return '(' + s + ')';
|
||||
}
|
||||
|
||||
// 将正则表达式合并为一个联合的正则表达式模式
|
||||
function getRegUnion(regexps: string[]): string {
|
||||
if (!regexps.length) {
|
||||
return '(?!)';
|
||||
}
|
||||
const source = regexps.map(s => '(?:' + s + ')').join('|');
|
||||
return '(?:' + source + ')';
|
||||
}
|
||||
|
||||
function getReg(input: string | Record<string, any>): string {
|
||||
if (typeof input === 'string') {
|
||||
return '(?:' + transferReg(input) + ')';
|
||||
} else if (checkRegExp(input) || checkObject(input)) {
|
||||
if (input.ignoreCase) {
|
||||
throw new Error('/i 标志禁止使用');
|
||||
}
|
||||
if (input.global) {
|
||||
throw new Error('/g 标志禁止使用');
|
||||
}
|
||||
if (input.sticky) {
|
||||
throw new Error('/y 标志禁止使用');
|
||||
}
|
||||
if (input.multiline) {
|
||||
throw new Error('/m 标志禁止使用');
|
||||
}
|
||||
return input.source;
|
||||
} else {
|
||||
throw new Error(`${input}不符合规范!`);
|
||||
}
|
||||
}
|
||||
|
||||
function getRulesByObject(object: Record<string, any>) {
|
||||
const keys = Object.getOwnPropertyNames(object);
|
||||
|
||||
// 存储最终的规则数组
|
||||
const result: any[] = [];
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
const thing = object[key];
|
||||
|
||||
// 将属性值转换为规则数组
|
||||
const rules = [].concat(thing);
|
||||
|
||||
// 如果属性名为 'include',表示需要包含其他规则
|
||||
if (key === 'include') {
|
||||
for (let j = 0; j < rules.length; j++) {
|
||||
result.push({ include: rules[j] });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 用于保存当前规则的匹配模式
|
||||
let match = [];
|
||||
rules.forEach(function (rule) {
|
||||
if (checkObject(rule)) {
|
||||
// 如果规则是一个对象,表示具有选项设置,添加该规则到结果数组中,并重置匹配模式数组
|
||||
if (match.length) result.push(getRuleOptions(key, match));
|
||||
result.push(getRuleOptions(key, rule));
|
||||
match = [];
|
||||
} else {
|
||||
match.push(rule);
|
||||
}
|
||||
});
|
||||
|
||||
// 如果匹配模式数组中还有剩余的匹配模式,创建规则对象并添加到结果数组中
|
||||
if (match.length) result.push(getRuleOptions(key, match));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRulesByArray(array: any[]) {
|
||||
const result: any[] = [];
|
||||
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const obj = array[i];
|
||||
|
||||
// 如果元素具有 'include' 属性,表示需要包含其他规则
|
||||
if (obj.include) {
|
||||
const include = [].concat(obj.include);
|
||||
for (let j = 0; j < include.length; j++) {
|
||||
result.push({ include: include[j] });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!obj.type) {
|
||||
throw new Error('Rule 没有 type 属性');
|
||||
}
|
||||
result.push(getRuleOptions(obj.type, obj));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRuleOptions(type, obj) {
|
||||
// 如果 obj 不是一个对象,则将其转换为包含 'match' 属性的对象
|
||||
if (!checkObject(obj)) {
|
||||
obj = { match: obj };
|
||||
}
|
||||
|
||||
// 如果 obj 包含 'include' 属性,则抛出错误,因为匹配规则不能包含状态
|
||||
if (obj.include) {
|
||||
throw new Error('匹配规则不能包含状态!');
|
||||
}
|
||||
|
||||
// 创建默认的选项对象,初始化各个选项属性
|
||||
const options: Record<string, any> = {
|
||||
defaultType: type,
|
||||
lineBreaks: !!obj.error || !!obj.fallback,
|
||||
pop: false,
|
||||
next: null,
|
||||
push: null,
|
||||
error: false,
|
||||
fallback: false,
|
||||
value: null,
|
||||
type: null,
|
||||
shouldThrow: false,
|
||||
};
|
||||
|
||||
Object.assign(options, obj);
|
||||
|
||||
if (typeof options.type === 'string' && type !== options.type) {
|
||||
throw new Error('type 属性不能为字符串!');
|
||||
}
|
||||
|
||||
const match = options.match;
|
||||
if (Array.isArray(match)) {
|
||||
options.match = match;
|
||||
} else if (match) {
|
||||
options.match = [match];
|
||||
} else {
|
||||
options.match = [];
|
||||
}
|
||||
options.match.sort((a, b) => {
|
||||
// 根据规则的类型进行排序,确保正则表达式排在最前面,长度较长的规则排在前面
|
||||
if (checkRegExp(a) && checkRegExp(b)) {
|
||||
return 0;
|
||||
} else if (checkRegExp(b)) {
|
||||
return -1;
|
||||
} else if (checkRegExp(a)) {
|
||||
return +1;
|
||||
} else {
|
||||
return b.length - a.length;
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function getRules(spec) {
|
||||
return Array.isArray(spec) ? getRulesByArray(spec) : getRulesByObject(spec);
|
||||
}
|
||||
|
||||
const ruleUtils = {
|
||||
checkObject,
|
||||
checkRegExp,
|
||||
transferReg,
|
||||
checkSticky,
|
||||
getRegGroups,
|
||||
getRegCapture,
|
||||
getRegUnion,
|
||||
getReg,
|
||||
getRulesByObject,
|
||||
getRulesByArray,
|
||||
getRuleOptions,
|
||||
getRules,
|
||||
};
|
||||
|
||||
export default ruleUtils;
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import { CompiledMessage } from "../types/types";
|
||||
import parse from "../parser/parser";
|
||||
import getTokenAST from "./getTokenAST";
|
||||
import I18n from "../core/I18n";
|
||||
|
||||
|
||||
|
||||
export function isVariantI18n(i18n?: I18n) {
|
||||
if (!i18n) {
|
||||
throw new Error(`I18n object is not found!`);
|
||||
}
|
||||
}
|
||||
|
||||
function generateKey<T>(locales?: string | string[], options: T = {} as T) {
|
||||
const localeKey = Array.isArray(locales) ? locales.sort().join('-') : locales;
|
||||
return `${localeKey}:${JSON.stringify(options)}`;
|
||||
}
|
||||
|
||||
function compile(message: string): CompiledMessage {
|
||||
try {
|
||||
return getTokenAST(parse(message));
|
||||
} catch (e) {
|
||||
console.error(`Message cannot be parse due to syntax errors: ${message}`);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
const utils = {
|
||||
isVariantI18n,
|
||||
generateKey,
|
||||
compile,
|
||||
}
|
||||
|
||||
export default utils;
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import I18n from '../../src/core/I18n';
|
||||
|
||||
describe('I18n', () => {
|
||||
it('load catalog and merge with existing', () => {
|
||||
const i18n = new I18n({});
|
||||
const messages = {
|
||||
Hello: 'Hello',
|
||||
};
|
||||
|
||||
i18n.loadMessage('en', messages);
|
||||
i18n.changeLanguage('en');
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
i18n.loadMessage('fr', { Hello: 'Salut' });
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
});
|
||||
|
||||
it('should load multiple language ', function () {
|
||||
const enMessages = {
|
||||
Hello: 'Hello',
|
||||
};
|
||||
const frMessage = {
|
||||
Hello: 'Salut',
|
||||
};
|
||||
const intl = new I18n({});
|
||||
intl.loadMessage({
|
||||
en: enMessages,
|
||||
fr: frMessage,
|
||||
});
|
||||
intl.changeLanguage('en');
|
||||
expect(intl.messages).toEqual(enMessages);
|
||||
|
||||
intl.changeLanguage('fr');
|
||||
expect(intl.messages).toEqual(frMessage);
|
||||
});
|
||||
|
||||
it('should switch active locale', () => {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
};
|
||||
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
messages: {
|
||||
fr: messages,
|
||||
en: {},
|
||||
},
|
||||
});
|
||||
|
||||
expect(i18n.locale).toEqual('en');
|
||||
expect(i18n.messages).toEqual({});
|
||||
|
||||
i18n.changeLanguage('fr');
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
});
|
||||
|
||||
it('should switch active locale', () => {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
};
|
||||
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: messages,
|
||||
fr:{}
|
||||
},
|
||||
});
|
||||
|
||||
i18n.changeLanguage('en');
|
||||
expect(i18n.locale).toEqual('en');
|
||||
expect(i18n.messages).toEqual(messages);
|
||||
i18n.changeLanguage('fr');
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.messages).toEqual({});
|
||||
});
|
||||
it('._ allow escaping syntax characters', () => {
|
||||
const messages = {
|
||||
"My ''name'' is '{name}'": "Mi ''nombre'' es '{name}'",
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'es',
|
||||
messages: { es: messages },
|
||||
});
|
||||
expect(i18n.formatMessage("My ''name'' is '{name}'")).toEqual("Mi 'nombre' es {name}");
|
||||
});
|
||||
|
||||
it('._ should format message from catalog', function () {
|
||||
const messages = {
|
||||
Hello: 'Salut',
|
||||
id: "Je m'appelle {name}",
|
||||
};
|
||||
const i18n = new I18n({
|
||||
locale: 'fr',
|
||||
messages: { fr: messages },
|
||||
});
|
||||
expect(i18n.locale).toEqual('fr');
|
||||
expect(i18n.formatMessage('Hello')).toEqual('Salut');
|
||||
expect(i18n.formatMessage('id', { name: 'Fred' })).toEqual("Je m'appelle Fred");
|
||||
});
|
||||
|
||||
it('should return the formatted date and time', () => {
|
||||
const i18n = new I18n({
|
||||
locale: 'fr',
|
||||
});
|
||||
const formattedDateTime = i18n.formatDate('2023-06-06T07:53:54.465Z', {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'short',
|
||||
});
|
||||
expect(typeof formattedDateTime).toBe('string');
|
||||
expect(formattedDateTime).toEqual('mardi 6 juin 2023 à 15:53');
|
||||
});
|
||||
|
||||
it('should return the formatted number', () => {
|
||||
const i18n = new I18n({
|
||||
locale: 'en',
|
||||
});
|
||||
const formattedNumber = i18n.formatNumber(123456.789, { style: 'currency', currency: 'USD' });
|
||||
expect(typeof formattedNumber).toBe('string');
|
||||
expect(formattedNumber).toEqual('$123,456.79');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import * as React from 'react';
|
||||
import I18nProvider from '../../../src/core/components/I18nProvider';
|
||||
import { FormattedMessage } from '../../../index';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import {createI18nInstance} from "../../../src/core/I18n";
|
||||
|
||||
const dummyContext = React.createContext('');
|
||||
const { Provider: DummyProvider, Consumer: DummyConsumer } = dummyContext;
|
||||
|
||||
describe('<FormattedMessage>', () => {
|
||||
const enMessage = {
|
||||
hello: '你好',
|
||||
id: "Je m'appelle {name}",
|
||||
};
|
||||
const locale = 'en';
|
||||
|
||||
const i18n = createI18nInstance({
|
||||
locale: locale,
|
||||
messages: enMessage,
|
||||
});
|
||||
it('should format context', function () {
|
||||
const { getByTestId } = render(
|
||||
<I18nProvider key={locale} locale={locale} messages={enMessage}>
|
||||
<span data-testid="id">
|
||||
<FormattedMessage data-testid="id" id={enMessage.hello} />
|
||||
</span>
|
||||
</I18nProvider>
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('hello', '', {}));
|
||||
}, 1000);
|
||||
});
|
||||
it('should format context', function () {
|
||||
const props = {
|
||||
id: enMessage.id,
|
||||
values: { name: 'fred' },
|
||||
};
|
||||
const { getByTestId } = render(
|
||||
<I18nProvider key={locale} locale={'en'} messages={enMessage}>
|
||||
<span data-testid="id">
|
||||
<FormattedMessage data-testid="id" id={props.id} values={props.values} />
|
||||
</span>
|
||||
</I18nProvider>
|
||||
);
|
||||
expect(getByTestId('id')).toHaveTextContent(i18n.formatMessage('id', { name: 'fred' }, {}));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { IntlProvider } from '../../../index';
|
||||
import {createI18nInstance} from "../../../src/core/I18n";
|
||||
|
||||
describe('I18nProvider', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
const locale = 'en';
|
||||
const i18n = createI18nInstance({
|
||||
locale: locale,
|
||||
});
|
||||
it('should re-render on locale changes', () => {
|
||||
const CurrentLocale = () => {
|
||||
return <span>{i18n.locale}</span>;
|
||||
};
|
||||
const { container } = render(
|
||||
<IntlProvider key={locale} locale={locale} messages={{}}>
|
||||
<CurrentLocale />
|
||||
</IntlProvider>
|
||||
);
|
||||
// First render — no output, because locale isn't activated
|
||||
expect(container.textContent).toEqual('en');
|
||||
|
||||
// act函数值当组件需要被重新渲染的时候进行调度
|
||||
act(() => {
|
||||
i18n.loadMessage('en', {});
|
||||
});
|
||||
|
||||
expect(container.textContent).toEqual('en');
|
||||
|
||||
act(() => {
|
||||
i18n.loadMessage('cs', {});
|
||||
i18n.changeLanguage('cs');
|
||||
});
|
||||
// After loading and activating locale, it's finally rendered.
|
||||
setTimeout(() => {
|
||||
expect(container.textContent).toEqual('cs');
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('should subscribe for locale changes', () => {
|
||||
const i18n = createI18nInstance();
|
||||
i18n.on = jest.fn(() => jest.fn());
|
||||
expect(i18n.on).not.toBeCalled();
|
||||
render(
|
||||
<IntlProvider
|
||||
key={locale}
|
||||
locale={locale}
|
||||
messages={{}}
|
||||
>
|
||||
<p />
|
||||
</IntlProvider>
|
||||
);
|
||||
setTimeout(() => {
|
||||
expect(i18n.on).toBeCalledWith('change', expect.anything());
|
||||
}, 1000);
|
||||
});
|
||||
it('should subscribe for locale changes when param i18n', () => {
|
||||
const i18n = createI18nInstance();
|
||||
i18n.on = jest.fn(() => jest.fn());
|
||||
expect(i18n.on).not.toBeCalled();
|
||||
render(
|
||||
<IntlProvider i18n={i18n}>
|
||||
<p />
|
||||
</IntlProvider>
|
||||
);
|
||||
setTimeout(() => {
|
||||
expect(i18n.on).toBeCalledWith('change', expect.anything());
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
it('should render children', () => {
|
||||
const child = <div data-testid="child" />;
|
||||
|
||||
const { getByTestId } = render(
|
||||
<IntlProvider key={locale} locale={locale} messages={{}}>
|
||||
{child}
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId('child')).toBeTruthy(); // toBeTruthy()匹配任何if语句为真
|
||||
});
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { injectIntl, IntlProvider } from '../../../index';
|
||||
import { render } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const mountWithProvider = (el: JSX.Element) => render(<IntlProvider locale="en">{el}</IntlProvider>);
|
||||
|
||||
describe('InjectIntl', () => {
|
||||
let Wrapped;
|
||||
|
||||
beforeEach(() => {
|
||||
Wrapped = ({ i18n }: { i18n }) => <div data-testid="test">{JSON.stringify(i18n)}</div>;
|
||||
Wrapped.someStatic = {
|
||||
type: true,
|
||||
};
|
||||
});
|
||||
|
||||
it('allows introspection access to the wrapped component', () => {
|
||||
expect((injectIntl(Wrapped) as any).WrappedComponent).toBe(Wrapped);
|
||||
});
|
||||
|
||||
it(' should copy statics', () => {
|
||||
expect((injectIntl(Wrapped) as any).someStatic.type).toBe(true);
|
||||
});
|
||||
|
||||
it('throws when InjectI18n is missing from ancestry', () => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const Injected = injectIntl(Wrapped);
|
||||
|
||||
// @ts-ignore
|
||||
expect(() => render(<Injected />)).toThrow("Cannot read properties of null (reading 'i18n')");
|
||||
});
|
||||
|
||||
it('should contain all props in WrappedComponent when use InjectI18n', () => {
|
||||
const Injected = injectIntl(Wrapped) as any;
|
||||
const props = {
|
||||
foo: 'bar',
|
||||
};
|
||||
|
||||
const { getByTestId } = mountWithProvider(<Injected {...props} />);
|
||||
expect(getByTestId('test')).toHaveTextContent('{"_events":{},"locale":"en","locales":["en"],"allMessages":{},"_localeData":{}}');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import createI18n from '../../src/core/createI18n';
|
||||
|
||||
describe('createI18n', () => {
|
||||
it('createIntl', function () {
|
||||
const i18n = createI18n({
|
||||
locale: 'en',
|
||||
messages: {
|
||||
foo: 'bar',
|
||||
},
|
||||
});
|
||||
expect(
|
||||
i18n.formatMessage({
|
||||
id: 'foo',
|
||||
})
|
||||
).toBe('bar');
|
||||
});
|
||||
|
||||
it('should not warn when defaultRichTextElements is not used', function () {
|
||||
const onWarn = jest.fn();
|
||||
createI18n({
|
||||
locale: 'en',
|
||||
messages: {
|
||||
foo: 'bar',
|
||||
},
|
||||
onWarn,
|
||||
});
|
||||
expect(onWarn).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import * as React from 'react'
|
||||
import {render} from '@testing-library/react'
|
||||
import {IntlProvider, useIntl} from "../../../index";
|
||||
|
||||
const FunctionComponent = ({spy}: { spy?: Function }) => {
|
||||
const {i18n} = useIntl()
|
||||
spy!(i18n.locale)
|
||||
return null
|
||||
}
|
||||
|
||||
const FC = () => {
|
||||
const i18n = useIntl()
|
||||
return i18n.formatNumber(10000, {style: 'currency', currency: 'USD'}) as any
|
||||
}
|
||||
|
||||
describe('useIntl() hooks', () => {
|
||||
it('throws when <IntlProvider> is missing from ancestry', () => {
|
||||
// So it doesn't spam the console
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {
|
||||
})
|
||||
expect(() => render(<FunctionComponent/>)).toThrow(
|
||||
'I18n object is not found!'
|
||||
)
|
||||
})
|
||||
|
||||
it('hooks onto the intl context', () => {
|
||||
const spy = jest.fn()
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<FunctionComponent spy={spy}/>
|
||||
</IntlProvider>
|
||||
)
|
||||
expect(spy).toHaveBeenCalledWith('en')
|
||||
})
|
||||
|
||||
it('should work when switching locale on provider', () => {
|
||||
const {rerender, getByTestId} = render(
|
||||
<IntlProvider locale="en">
|
||||
<span data-testid="comp">
|
||||
<FC/>
|
||||
</span>
|
||||
</IntlProvider>
|
||||
)
|
||||
expect(getByTestId('comp')).toMatchSnapshot()
|
||||
rerender(
|
||||
<IntlProvider locale="es">
|
||||
<span data-testid="comp">
|
||||
<FC/>
|
||||
</span>
|
||||
</IntlProvider>
|
||||
)
|
||||
expect(getByTestId('comp')).toMatchSnapshot()
|
||||
rerender(
|
||||
<IntlProvider locale="en">
|
||||
<span data-testid="comp">
|
||||
<FC/>
|
||||
</span>
|
||||
</IntlProvider>
|
||||
)
|
||||
|
||||
expect(getByTestId('comp')).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import Translation from '../../src/format/Translation';
|
||||
|
||||
describe('Translation', () => {
|
||||
let translation;
|
||||
beforeEach(() => {
|
||||
// 在每个测试之前创建一个新的 Translation 实例
|
||||
const compiledMessage = ['Hello, ', ['name', 'text', null], '!'];
|
||||
const locale = 'en';
|
||||
const locales = {};
|
||||
const localeConfig = {};
|
||||
const useMemorize = true;
|
||||
translation = new Translation(compiledMessage, locale, locales, localeConfig, useMemorize);
|
||||
});
|
||||
describe('formatMessage', () => {
|
||||
it('should return the message if it is not an array', () => {
|
||||
const result = translation.formatMessage('test message', jest.fn());
|
||||
expect(result).toBe('test message');
|
||||
});
|
||||
|
||||
it('should concatenate string tokens in the message array', () => {
|
||||
const result = translation.formatMessage(['Hello, ', 'World!'], jest.fn());
|
||||
expect(result).toBe('Hello, World!');
|
||||
});
|
||||
|
||||
it('should handle token arrays and use ctx to get the value', () => {
|
||||
const ctx = jest.fn().mockReturnValue('Hello');
|
||||
const result = translation.formatMessage([['name', 'type', 'format']], ctx);
|
||||
expect(result).toBe('Hello');
|
||||
expect(ctx).toHaveBeenCalledWith('name', 'type', 'format');
|
||||
});
|
||||
|
||||
it('should skip null values returned by ctx', () => {
|
||||
const ctx = jest.fn().mockReturnValue(null);
|
||||
const result = translation.formatMessage([['name', 'type', 'format'], 'World!'], ctx);
|
||||
expect(result).toBe('World!');
|
||||
});
|
||||
|
||||
it('should handle nested formats in the token array', () => {
|
||||
const ctx = jest.fn((name, type, format) => format.value);
|
||||
const formatObject = {
|
||||
value: 'Hello',
|
||||
};
|
||||
const result = translation.formatMessage([['name', 'type', formatObject], ', World!'], ctx);
|
||||
expect(result).toBe('Hello, World!');
|
||||
});
|
||||
it('should return a string when compiledMessage is a string', () => {
|
||||
const compiledMessage = 'Hello, world!';
|
||||
const textFormatter = jest.fn();
|
||||
const result = translation.formatMessage(compiledMessage, textFormatter);
|
||||
|
||||
expect(result).toBe('Hello, world!');
|
||||
expect(textFormatter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should format a message with placeholders', () => {
|
||||
const compiledMessage = ['Hello, ', ['name', 'text', null], '!'];
|
||||
// @ts-ignore
|
||||
const textFormatter = jest.fn((name, type, format) => {
|
||||
if (name === 'name') {
|
||||
return 'John';
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const result = translation.formatMessage(compiledMessage, textFormatter);
|
||||
|
||||
expect(result).toBe('Hello, John!');
|
||||
expect(textFormatter).toHaveBeenCalledWith('name', 'text', null);
|
||||
expect(textFormatter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should format a message with formatted placeholders', () => {
|
||||
const compiledMessage = ['Hello, ', ['name', 'text', { text: 'Lowercase' }], '!'];
|
||||
// @ts-ignore
|
||||
const textFormatter = jest.fn((name, type, format) => {
|
||||
if (name === 'name') {
|
||||
return 'John';
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const result = translation.formatMessage(compiledMessage, textFormatter);
|
||||
|
||||
expect(result).toBe('Hello, John!');
|
||||
expect(textFormatter).toHaveBeenCalledWith('name', 'text', { text: 'Lowercase' });
|
||||
expect(textFormatter).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should recursively format a message with nested placeholders', () => {
|
||||
const compiledMessage = [
|
||||
'Hello, ',
|
||||
['name', 'text', null],
|
||||
'! Your age is ',
|
||||
['age', 'number', { style: 'decimal' }],
|
||||
'.',
|
||||
];
|
||||
// @ts-ignore
|
||||
const textFormatter = jest.fn((name, type, format) => {
|
||||
if (name === 'name') {
|
||||
return 'John';
|
||||
} else if (name === 'age') {
|
||||
return '30';
|
||||
}
|
||||
return "";
|
||||
});
|
||||
const result = translation.formatMessage(compiledMessage, textFormatter);
|
||||
|
||||
expect(result).toBe('Hello, John! Your age is 30.');
|
||||
expect(textFormatter).toHaveBeenCalledWith('name', 'text', null);
|
||||
expect(textFormatter).toHaveBeenCalledWith('age', 'number', { style: 'decimal' });
|
||||
expect(textFormatter).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import creatI18nCache from '../../../src/format/cache/cache';
|
||||
|
||||
describe('creatI18nCache', () => {
|
||||
it('should create an empty IntlCache object', () => {
|
||||
const intlCache = creatI18nCache();
|
||||
|
||||
expect(intlCache).toEqual({
|
||||
dateTimeFormat: {},
|
||||
numberFormat: {},
|
||||
messages: {},
|
||||
plurals: {},
|
||||
select: {},
|
||||
octothorpe: {},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import Translation from '../../src/format/Translation';
|
||||
import utils from '../../src/utils/utils';
|
||||
|
||||
describe('compile', function () {
|
||||
const englishPlurals = {
|
||||
plurals(value, ordinal) {
|
||||
if (ordinal) {
|
||||
return (
|
||||
{
|
||||
'1': 'one',
|
||||
'2': 'two',
|
||||
'3': 'few',
|
||||
}[value] || 'other'
|
||||
);
|
||||
} else {
|
||||
return value === 1 ? 'one' : 'other';
|
||||
}
|
||||
},
|
||||
};
|
||||
const prepare = (message, locale?, locales?) => {
|
||||
const translation = new Translation(utils.compile(message), locale || 'en', locales, englishPlurals);
|
||||
return translation.translate.bind(translation);
|
||||
};
|
||||
|
||||
it('should compile message with variable', function () {
|
||||
const cache = utils.compile('Hey {name}!');
|
||||
expect(new Translation(cache, 'en', [], {}).translate({ name: 'Joe' })).toEqual('Hey Joe!');
|
||||
});
|
||||
|
||||
it('should compile select', function () {
|
||||
const translate = prepare('{value, select, female {She} other {They}}');
|
||||
expect(translate({ value: 'female' })).toEqual('She');
|
||||
expect(translate({ value: 'n/a' })).toEqual('They');
|
||||
});
|
||||
|
||||
it('should compile plurals', function () {
|
||||
const translate = prepare('{value, plural, one {{value} Book} other {# Books}}');
|
||||
expect(translate({ value: 1 })).toEqual('1 Book');
|
||||
expect(translate({ value: 2 })).toEqual('2 Books');
|
||||
|
||||
|
||||
const translatePlurals = prepare('{value, plural, offset:1 =0 {No Books} one {# Book} other {# Books}}');
|
||||
|
||||
expect(translatePlurals({ value: 0 })).toEqual('No Books');
|
||||
expect(translatePlurals({ value: 2 })).toEqual('1 Book');
|
||||
expect(translatePlurals({ value: 3 })).toEqual('2 Books');
|
||||
});
|
||||
|
||||
it('should compile selectordinal', function () {
|
||||
const translateSelectordinal = prepare('{value, selectordinal, one {#st Book} two {#nd Book}}');
|
||||
expect(translateSelectordinal({ value: 1 })).toEqual('1st Book');
|
||||
expect(translateSelectordinal({ value: 2 })).toEqual('2nd Book');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import { DateTimeFormatter } from '../../../index';
|
||||
|
||||
describe('DateTimeFormatter', () => {
|
||||
const date = new Date('2023-04-03T12:34:56Z');
|
||||
it('date formatter is memoized', async () => {
|
||||
const firstRunt0 = performance.now();
|
||||
const dateTimeFormatter1 = new DateTimeFormatter('es', {});
|
||||
dateTimeFormatter1.dateTimeFormat(new Date());
|
||||
const firstRunt1 = performance.now();
|
||||
const firstRunResult = firstRunt1 - firstRunt0;
|
||||
|
||||
const seconddRunt0 = performance.now();
|
||||
const dateTimeFormatter2 = new DateTimeFormatter('es', {}, false);
|
||||
dateTimeFormatter2.dateTimeFormat(new Date());
|
||||
const seconddRunt1 = performance.now();
|
||||
const secondRunResult = seconddRunt1 - seconddRunt0;
|
||||
|
||||
expect(secondRunResult).toBeLessThan(firstRunResult);
|
||||
});
|
||||
|
||||
it('should format a date using default options', () => {
|
||||
const formatted = new DateTimeFormatter('en').dateTimeFormat(date);
|
||||
expect(formatted).toEqual('4/3/2023');
|
||||
});
|
||||
|
||||
it('should format a date using custom options', () => {
|
||||
const formatted = new DateTimeFormatter('en-US', { weekday: 'long' }).dateTimeFormat(date);
|
||||
expect(formatted).toEqual('Monday');
|
||||
});
|
||||
|
||||
it('should parse a date string and format it', () => {
|
||||
const formatted = new DateTimeFormatter('en-US').dateTimeFormat(date.toISOString());
|
||||
expect(formatted).toEqual('4/3/2023');
|
||||
});
|
||||
|
||||
it('should memoize formatter instances by options and locales', () => {
|
||||
const spy = jest.spyOn(Intl, 'DateTimeFormat');
|
||||
const formatter1 = new DateTimeFormatter('en-US', { month: 'short' });
|
||||
const formatter2 = new DateTimeFormatter('en-US', { month: 'short' });
|
||||
const formatter3 = new DateTimeFormatter('en-GB', { month: 'short' });
|
||||
formatter1.dateTimeFormat(date);
|
||||
formatter2.dateTimeFormat(date);
|
||||
formatter3.dateTimeFormat(date);
|
||||
expect(spy).toHaveBeenCalledWith('en-US', { month: 'short' });
|
||||
expect(spy).toHaveBeenCalledWith('en-GB', { month: 'short' });
|
||||
});
|
||||
|
||||
it('should not memoize formatter instances when memoize is false', () => {
|
||||
const spy = jest.spyOn(Intl, 'DateTimeFormat');
|
||||
const formatter1 = new DateTimeFormatter('en-US', { month: 'short' }, false);
|
||||
const formatter2 = new DateTimeFormatter('en-US', { month: 'short' }, false);
|
||||
formatter1.dateTimeFormat(date);
|
||||
formatter2.dateTimeFormat(date);
|
||||
expect(spy).toHaveBeenCalledTimes(5);
|
||||
expect(spy).toHaveBeenCalledWith('en-US', { month: 'short' });
|
||||
});
|
||||
it('should format a Date object correctly', () => {
|
||||
const formatter = new DateTimeFormatter('en-US');
|
||||
const date = new Date(2023, 0, 1);
|
||||
const formatted = formatter.dateTimeFormat(date);
|
||||
expect(formatted).toEqual('1/1/2023');
|
||||
});
|
||||
|
||||
it('should format a string representation of date correctly', () => {
|
||||
const formatter = new DateTimeFormatter('en-US');
|
||||
const dateString = '2023-01-01';
|
||||
const formatted = formatter.dateTimeFormat(dateString);
|
||||
expect(formatted).toEqual('1/1/2023');
|
||||
});
|
||||
|
||||
it('should format using specified format options', () => {
|
||||
const formatter = new DateTimeFormatter('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
const date = new Date(2023, 0, 1);
|
||||
const formatted = formatter.dateTimeFormat(date);
|
||||
expect(formatted).toEqual('January 1, 2023');
|
||||
});
|
||||
|
||||
it('should format using memorized formatter when useMemorize is true', () => {
|
||||
const formatter = new DateTimeFormatter('en-US',{"year":'numeric'}, true);
|
||||
const date = new Date(2023, 0, 1);
|
||||
const formatted1 = formatter.dateTimeFormat(date);
|
||||
const formatted2 = formatter.dateTimeFormat(date);
|
||||
expect(formatted1).toEqual(formatted2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import { NumberFormatter } from '../../../index';
|
||||
|
||||
describe('NumberFormatter', () => {
|
||||
it('number formatter is memoized', async () => {
|
||||
const firstRunt0 = performance.now();
|
||||
const numberFormatter1 = new NumberFormatter('es', {});
|
||||
numberFormatter1.numberFormat(10000);
|
||||
const firstRunt1 = performance.now();
|
||||
const firstRunResult = firstRunt1 - firstRunt0;
|
||||
|
||||
const seconddRunt0 = performance.now();
|
||||
const numberFormatter = new NumberFormatter('es', {}, false);
|
||||
numberFormatter.numberFormat(10000);
|
||||
const secondRunt1 = performance.now();
|
||||
const secondRunResult = secondRunt1 - seconddRunt0;
|
||||
|
||||
expect(secondRunResult).toBeLessThan(firstRunResult);
|
||||
});
|
||||
|
||||
it('formats a number with default options', () => {
|
||||
const format = new NumberFormatter('en-US');
|
||||
expect(format.numberFormat(1000)).toBe('1,000');
|
||||
});
|
||||
|
||||
it('formats a number with custom options', () => {
|
||||
const format = new NumberFormatter('en-US', { style: 'currency', currency: 'USD' });
|
||||
expect(format.numberFormat(1000)).toBe('$1,000.00');
|
||||
});
|
||||
|
||||
it('does not memoize the formatter with different options', () => {
|
||||
const format1 = new NumberFormatter('en-US', { style: 'currency', currency: 'USD' });
|
||||
const format2 = new NumberFormatter('en-US', { style: 'currency', currency: 'EUR' });
|
||||
expect(format1).not.toBe(format2);
|
||||
});
|
||||
|
||||
it('formats negative numbers', () => {
|
||||
const format = new NumberFormatter('en-US', { style: 'currency', currency: 'USD' });
|
||||
expect(format.numberFormat(-1000)).toBe('-$1,000.00');
|
||||
});
|
||||
|
||||
it('formats numbers with different locales', () => {
|
||||
const format1 = new NumberFormatter('en-US', { style: 'currency', currency: 'USD' });
|
||||
const format2 = new NumberFormatter('fr-FR', { style: 'currency', currency: 'EUR' });
|
||||
expect(format1.numberFormat(1000)).toBe('$1,000.00');
|
||||
|
||||
});
|
||||
it('should format a positive number correctly', () => {
|
||||
const formatter = new NumberFormatter('en-US');
|
||||
const number = 12345.6789;
|
||||
const formatted = formatter.numberFormat(number);
|
||||
expect(formatted).toEqual('12,345.679');
|
||||
});
|
||||
|
||||
it('should format a negative number correctly', () => {
|
||||
const formatter = new NumberFormatter('en-US');
|
||||
const number = -12345.6789;
|
||||
const formatted = formatter.numberFormat(number);
|
||||
expect(formatted).toEqual('-12,345.679');
|
||||
});
|
||||
|
||||
it('should format using specified format options', () => {
|
||||
const formatOptions = { style: 'currency', currency: 'EUR' };
|
||||
const formatter = new NumberFormatter('en-US', formatOptions);
|
||||
const number = 12345.6789;
|
||||
const formatted = formatter.numberFormat(number);
|
||||
expect(formatted).toEqual('€12,345.68');
|
||||
});
|
||||
|
||||
it('should format using memorized formatter when useMemorize is true', () => {
|
||||
const formatter = new NumberFormatter('en-US', undefined, true);
|
||||
const number = 12345.6789;
|
||||
const formatted1 = formatter.numberFormat(number);
|
||||
const formatted2 = formatter.numberFormat(number);
|
||||
expect(formatted1).toEqual(formatted2);
|
||||
});
|
||||
|
||||
it('should create a new formatter when useMemorize is false', () => {
|
||||
const formatter = new NumberFormatter('en-US', undefined, false);
|
||||
const number = 12345.6789;
|
||||
const formatted1 = formatter.numberFormat(number);
|
||||
const formatted2 = formatter.numberFormat(number);
|
||||
expect(formatted1).toEqual(formatted2);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import {getFormatMessage} from "../../src/format/getFormatMessage";
|
||||
import I18n from "../../src/core/I18n";
|
||||
|
||||
describe('getFormatMessage', () => {
|
||||
// Mocked i18nInstance object
|
||||
const i18nInstance = new I18n({
|
||||
messages: {
|
||||
en: {
|
||||
greeting: 'Hello, {name}!',
|
||||
},
|
||||
},
|
||||
locale: 'en',
|
||||
error: "missingMessage"
|
||||
});
|
||||
|
||||
it('should return the correct translation for an existing message ID', () => {
|
||||
const id = 'greeting';
|
||||
const values = {name: 'John'};
|
||||
const expectedResult = 'Hello, John!';
|
||||
|
||||
const result = getFormatMessage(i18nInstance, id, values);
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
it('should return the default message when the translation is missing', () => {
|
||||
const id = 'missingMessage';
|
||||
const expectedResult = 'missingMessage';
|
||||
|
||||
const result = getFormatMessage(i18nInstance, id);
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import { defineMessage, defineMessages } from '../index';
|
||||
|
||||
describe('index', () => {
|
||||
describe('defineMessages Test', () => {
|
||||
it('return the input message object', () => {
|
||||
const messages = {
|
||||
greeting: {
|
||||
id: 'greeting',
|
||||
defaultMessage: 'Hello',
|
||||
},
|
||||
farewell: {
|
||||
id: 'farewell',
|
||||
defaultMessage: 'Goodbye',
|
||||
},
|
||||
};
|
||||
|
||||
const result = defineMessages(messages);
|
||||
|
||||
expect(result).toEqual(messages);
|
||||
});
|
||||
});
|
||||
|
||||
describe('defineMessage Test', () => {
|
||||
it('return the input message object', () => {
|
||||
const message = {
|
||||
id: 'greeting',
|
||||
defaultMessage: 'Hello',
|
||||
};
|
||||
|
||||
const result = defineMessage(message);
|
||||
|
||||
expect(result).toEqual(message);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import { checkSelectType } from '../../src/parser/parser';
|
||||
|
||||
describe('checkSelectType function', () => {
|
||||
it('should return true for valid select types', () => {
|
||||
expect(checkSelectType('plural')).toBe(true);
|
||||
expect(checkSelectType('select')).toBe(true);
|
||||
expect(checkSelectType('selectordinal')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for invalid select types', () => {
|
||||
expect(checkSelectType('invalid')).toBe(false);
|
||||
expect(checkSelectType('other')).toBe(false);
|
||||
expect(checkSelectType('')).toBe(false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import { checkStateGroup } from '../../src/parser/parseMappingRule';
|
||||
|
||||
describe('checkStateGroup function', () => {
|
||||
it('should throw an error if state is missing', () => {
|
||||
const group = { push: 'missingState' };
|
||||
const name = 'currentName';
|
||||
const map = { existingState: {} };
|
||||
|
||||
expect(() => {
|
||||
checkStateGroup(group, name, map);
|
||||
}).toThrowError('The state is missing.');
|
||||
});
|
||||
|
||||
it('should throw an error if pop is not 1', () => {
|
||||
const group = { pop: '2' };
|
||||
const name = 'currentName';
|
||||
const map = { existingState: {} };
|
||||
|
||||
expect(() => {
|
||||
checkStateGroup(group, name, map);
|
||||
}).toThrowError('The value of pop must be 1.');
|
||||
});
|
||||
|
||||
it('should not throw an error if state and pop are valid', () => {
|
||||
const group = { push: 'existingState', pop: '1' };
|
||||
const name = 'currentName';
|
||||
const map = { existingState: {} };
|
||||
|
||||
expect(() => {
|
||||
checkStateGroup(group, name, map);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import copyStaticProps from '../../src/utils/copyStaticProps';
|
||||
|
||||
describe('hoistNonReactStatics', () => {
|
||||
test('should hoist static properties from sourceComponent to targetComponent', () => {
|
||||
class SourceComponent {
|
||||
static staticProp = 'sourceProp';
|
||||
}
|
||||
|
||||
class TargetComponent {}
|
||||
|
||||
copyStaticProps(TargetComponent, SourceComponent);
|
||||
|
||||
expect((TargetComponent as any).staticProp).toBe('sourceProp');
|
||||
});
|
||||
|
||||
test('should hoist static properties from inherited components', () => {
|
||||
class SourceComponent {
|
||||
static staticProp = 'sourceProp';
|
||||
}
|
||||
|
||||
class InheritedComponent extends SourceComponent {}
|
||||
|
||||
class TargetComponent {}
|
||||
|
||||
copyStaticProps(TargetComponent, InheritedComponent);
|
||||
|
||||
expect((TargetComponent as any).staticProp).toBe('sourceProp');
|
||||
});
|
||||
|
||||
test('should not hoist properties if descriptor is not valid', () => {
|
||||
class SourceComponent {
|
||||
get staticProp() {
|
||||
return 'sourceProp';
|
||||
}
|
||||
}
|
||||
|
||||
class TargetComponent {}
|
||||
|
||||
copyStaticProps(TargetComponent, SourceComponent);
|
||||
|
||||
expect((TargetComponent as any).staticProp).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should not hoist properties if descriptor is not valid', () => {
|
||||
class SourceComponent {
|
||||
static get staticProp() {
|
||||
return 'sourceProp';
|
||||
}
|
||||
}
|
||||
|
||||
class TargetComponent {}
|
||||
|
||||
copyStaticProps(TargetComponent, SourceComponent);
|
||||
|
||||
expect((TargetComponent as any).staticProp).toBe('sourceProp');
|
||||
});
|
||||
|
||||
test('copyStaticProps should not copy static properties that already exist in target or source component', () => {
|
||||
const targetComponent = { staticProp: 'target' };
|
||||
const sourceComponent = { staticProp: 'source' };
|
||||
copyStaticProps(targetComponent, sourceComponent);
|
||||
expect(targetComponent.staticProp).toBe('source'); // The value should remain the same
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import EventEmitter from '../../src/utils/eventListener';
|
||||
|
||||
describe('eventEmitter', () => {
|
||||
it('should call registered event listeners on emit', async () => {
|
||||
const firstListener = jest.fn();
|
||||
const secondListener = jest.fn(() => 'return value is ignored');
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
emitter.on('test', firstListener);
|
||||
emitter.on('test', secondListener);
|
||||
|
||||
emitter.emit('test', 42);
|
||||
|
||||
expect(firstListener).toBeCalledWith(42);
|
||||
expect(secondListener).toBeCalledWith(42);
|
||||
});
|
||||
|
||||
it('should allow unsubscribing from events', () => {
|
||||
const listener = jest.fn();
|
||||
const emitter = new EventEmitter();
|
||||
|
||||
const unsubscribe = emitter.on('test', listener);
|
||||
emitter.emit('test', 42);
|
||||
expect(listener).toBeCalledWith(42);
|
||||
|
||||
listener.mockReset();
|
||||
unsubscribe();
|
||||
emitter.emit('test', 42);
|
||||
expect(listener).not.toBeCalled();
|
||||
});
|
||||
|
||||
it("should do nothing when even doesn't exist", () => {
|
||||
const unknown = jest.fn();
|
||||
|
||||
const emitter = new EventEmitter();
|
||||
// this should not throw
|
||||
emitter.emit('test', 42);
|
||||
// this should not throw
|
||||
emitter.removeListener('test', unknown);
|
||||
|
||||
emitter.on('test', jest.fn());
|
||||
// this should not throw
|
||||
emitter.removeListener('test', unknown);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import utils from '../../src/utils/utils';
|
||||
|
||||
describe('generateKey', () => {
|
||||
it('should generate a key for a single locale without options', () => {
|
||||
const result = utils.generateKey('en');
|
||||
expect(result).toBe('en:{}');
|
||||
});
|
||||
|
||||
it('should generate a key for multiple locales without options', () => {
|
||||
const result = utils.generateKey(['en', 'fr']);
|
||||
expect(result).toBe('en-fr:{}');
|
||||
});
|
||||
|
||||
it('should sort multiple locales before generating the key', () => {
|
||||
const result = utils.generateKey(['fr', 'en']);
|
||||
expect(result).toBe('en-fr:{}');
|
||||
});
|
||||
|
||||
it('should generate a key with options for a single locale', () => {
|
||||
const result = utils.generateKey('en', { foo: 'bar' });
|
||||
expect(result).toBe('en:{"foo":"bar"}');
|
||||
});
|
||||
|
||||
it('should generate a key with options for multiple locales', () => {
|
||||
const result = utils.generateKey(['en', 'fr'], { foo: 'bar' });
|
||||
expect(result).toBe('en-fr:{"foo":"bar"}');
|
||||
});
|
||||
|
||||
it('should sort multiple locales and consider options before generating the key', () => {
|
||||
const result = utils.generateKey(['fr', 'en'], { foo: 'bar' });
|
||||
expect(result).toBe('en-fr:{"foo":"bar"}');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
import getTokenAST from '../../src/utils/getTokenAST';
|
||||
import * as assert from 'assert';
|
||||
|
||||
describe('getTokenAST', () => {
|
||||
it('should return an array containing a string', () => {
|
||||
const result = getTokenAST(['Hello', 'world']);
|
||||
expect(result).toEqual(['Hello', 'world']);
|
||||
});
|
||||
|
||||
it('should handle octothorpe tokens', () => {
|
||||
const result = getTokenAST([{ type: 'octothorpe' }, 'Hello']);
|
||||
expect(result).toContain('#');
|
||||
});
|
||||
|
||||
it('should handle argument tokens', () => {
|
||||
const result = getTokenAST([{ type: 'argument', arg: 'foo' }]);
|
||||
expect(result).toEqual([['foo']]);
|
||||
});
|
||||
|
||||
it('should handle function tokens with param', () => {
|
||||
const result = getTokenAST([{ type: 'function', arg: 'foo', key: 'bar', param: { tokens: ['baz'] } }]);
|
||||
expect(result).toEqual([['foo', 'bar', 'baz']]);
|
||||
});
|
||||
|
||||
it('should handle function tokens without param', () => {
|
||||
const result = getTokenAST([{ type: 'function', arg: 'foo', key: 'bar' }]);
|
||||
expect(result).toEqual([['foo', 'bar']]);
|
||||
});
|
||||
|
||||
it('should handle other tokens with offset', () => {
|
||||
const result = getTokenAST([{ type: 'other', arg: 'foo', offset: '1', cases: [{ key: 'one', tokens: ['bar'] }] }]);
|
||||
expect(result).toEqual([['foo', 'other', { offset: 1, one: ['bar'] }]]);
|
||||
});
|
||||
|
||||
it('should handle other tokens without offset', () => {
|
||||
const result = getTokenAST([{ type: 'other', arg: 'foo', cases: [{ key: 'one', tokens: ['bar'] }] }]);
|
||||
expect(result).toEqual([['foo', 'other', { one: ['bar'] }]]);
|
||||
});
|
||||
it('returns [arg, key, param] if token type is "function"', () => {
|
||||
const tokens = [
|
||||
{
|
||||
type: 'function',
|
||||
arg: 'arg1',
|
||||
key: 'key1',
|
||||
param: { tokens: ['param1'] },
|
||||
},
|
||||
];
|
||||
const result = getTokenAST(tokens);
|
||||
expect(result).toEqual([['arg1', 'key1', 'param1']]);
|
||||
});
|
||||
it('If the input parameter is not an array, an error should be thrown.', () => {
|
||||
const input = 'invalid input';
|
||||
assert.throws(() => getTokenAST(input), Error);
|
||||
});
|
||||
it('应该返回包含字符串的数组', () => {
|
||||
const tokens = [
|
||||
'Hello',
|
||||
{ type: 'octothorpe' },
|
||||
{ type: 'argument', arg: 'name' },
|
||||
{
|
||||
type: 'function',
|
||||
arg: 'formatDate',
|
||||
key: 'date',
|
||||
param: {
|
||||
tokens: ['YYYY-MM-DD'],
|
||||
},
|
||||
},
|
||||
];
|
||||
const expected = ['Hello', '#', ['name'], ['formatDate', 'date', 'YYYY-MM-DD']];
|
||||
const result = getTokenAST(tokens);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
it('对于复杂的 tokens 数组,应该返回嵌套的格式化数组', () => {
|
||||
const expected = [
|
||||
'Hello',
|
||||
'#',
|
||||
['name'],
|
||||
['formatDate', 'date', 'YYYY-MM-DD'],
|
||||
[
|
||||
'formatNumber',
|
||||
'number',
|
||||
{
|
||||
cases: [
|
||||
{
|
||||
key: 'uppercase',
|
||||
tokens: [
|
||||
'TRUE',
|
||||
{
|
||||
plural: {
|
||||
cases: {
|
||||
'=0': 'zero',
|
||||
'=1': 'one',
|
||||
other: 'other',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'lowercase',
|
||||
tokens: [
|
||||
'lowercase',
|
||||
'none',
|
||||
{
|
||||
cases: {
|
||||
'=0': 'zero',
|
||||
'=1': 'one',
|
||||
other: 'other',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
offset: 1,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
const tokens = [
|
||||
'Hello',
|
||||
{ type: 'octothorpe' },
|
||||
{ type: 'argument', arg: 'name' },
|
||||
{
|
||||
type: 'function',
|
||||
arg: 'formatDate',
|
||||
key: 'date',
|
||||
param: {
|
||||
tokens: ['YYYY-MM-DD'],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'function',
|
||||
arg: 'formatNumber',
|
||||
key: 'number',
|
||||
param: {
|
||||
tokens: [
|
||||
{
|
||||
offset: 1,
|
||||
cases: [
|
||||
{
|
||||
key: 'uppercase',
|
||||
tokens: ['TRUE', { plural: { cases: { '=0': 'zero', '=1': 'one', other: 'other' } } }],
|
||||
},
|
||||
{
|
||||
key: 'lowercase',
|
||||
tokens: ['lowercase', 'none', { cases: { '=0': 'zero', '=1': 'one', other: 'other' } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const result = getTokenAST(tokens);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,269 @@
|
|||
/*
|
||||
* Copyright (c) Huawei Technologies Co., Ltd. 2023-2023. All rights reserved.
|
||||
*/
|
||||
|
||||
import ruleUtils from '../../src/utils/parseRuleUtils';
|
||||
|
||||
describe('ruleUtils test', () => {
|
||||
describe('getRegGroups function', () => {
|
||||
it('should return the correct number of capturing groups in the regular expression pattern', () => {
|
||||
const pattern1 = 'abc(def)ghi(jkl)mno';
|
||||
const pattern2 = 'abc(def)ghi';
|
||||
const pattern3 = 'abc';
|
||||
const pattern4 = '';
|
||||
|
||||
const result1 = ruleUtils.getRegGroups(pattern1);
|
||||
const result2 = ruleUtils.getRegGroups(pattern2);
|
||||
const result3 = ruleUtils.getRegGroups(pattern3);
|
||||
const result4 = ruleUtils.getRegGroups(pattern4);
|
||||
|
||||
expect(result1).toBe(2);
|
||||
expect(result2).toBe(1);
|
||||
expect(result3).toBe(0);
|
||||
expect(result4).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getReg', () => {
|
||||
it('should return the correct regular expression when input is a string', () => {
|
||||
const input1 = 'abc';
|
||||
const input2 = '^\\d+$';
|
||||
const input3 = '[a-zA-Z]';
|
||||
|
||||
const result1 = ruleUtils.getReg(input1);
|
||||
const result2 = ruleUtils.getReg(input2);
|
||||
const result3 = ruleUtils.getReg(input3);
|
||||
|
||||
expect(result1).toBe('(?:abc)');
|
||||
expect(result2).toBe('(?:\\^\\\\d\\+\\$)');
|
||||
expect(result3).toBe('(?:\\[a\\-zA\\-Z\\])');
|
||||
});
|
||||
|
||||
it('should throw an error when input is an invalid regular expression object', () => {
|
||||
const input = {
|
||||
source: 'abc',
|
||||
ignoreCase: true,
|
||||
global: false,
|
||||
sticky: false,
|
||||
multiline: true,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
ruleUtils.getReg(input);
|
||||
}).toThrowError('标志禁止使用');
|
||||
});
|
||||
|
||||
it('should throw an error when input regular expression object has forbidden flags', () => {
|
||||
const input1 = {
|
||||
source: 'abc',
|
||||
ignoreCase: true,
|
||||
global: false,
|
||||
sticky: false,
|
||||
multiline: false,
|
||||
};
|
||||
|
||||
const input2 = {
|
||||
source: 'abc',
|
||||
ignoreCase: false,
|
||||
global: true,
|
||||
sticky: false,
|
||||
multiline: false,
|
||||
};
|
||||
|
||||
const input3 = {
|
||||
source: 'abc',
|
||||
ignoreCase: false,
|
||||
global: false,
|
||||
sticky: true,
|
||||
multiline: false,
|
||||
};
|
||||
|
||||
const input4 = {
|
||||
source: 'abc',
|
||||
ignoreCase: false,
|
||||
global: false,
|
||||
sticky: false,
|
||||
multiline: true,
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
ruleUtils.getReg(input1);
|
||||
}).toThrowError('/i 标志禁止使用');
|
||||
|
||||
expect(() => {
|
||||
ruleUtils.getReg(input2);
|
||||
}).toThrowError('/g 标志禁止使用');
|
||||
|
||||
expect(() => {
|
||||
ruleUtils.getReg(input3);
|
||||
}).toThrowError('/y 标志禁止使用');
|
||||
|
||||
expect(() => {
|
||||
ruleUtils.getReg(input4);
|
||||
}).toThrowError('/m 标志禁止使用');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRegUnion function', () => {
|
||||
it('should return the correct regular expression union of input patterns', () => {
|
||||
const patterns1 = ['abc', 'def', 'ghi'];
|
||||
const patterns2 = ['\\d+', '[a-z]+', '[A-Z]+'];
|
||||
const patterns3 = [];
|
||||
|
||||
const result1 = ruleUtils.getRegUnion(patterns1);
|
||||
const result2 = ruleUtils.getRegUnion(patterns2);
|
||||
const result3 = ruleUtils.getRegUnion(patterns3);
|
||||
|
||||
expect(result1).toBe('(?:(?:abc)|(?:def)|(?:ghi))');
|
||||
expect(result2).toBe('(?:(?:\\d+)|(?:[a-z]+)|(?:[A-Z]+))');
|
||||
expect(result3).toBe('(?!)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRuleOptions function', () => {
|
||||
it('should return the correct options object with default type', () => {
|
||||
const type = 'content';
|
||||
const obj = { match: 'abc' };
|
||||
|
||||
const result = ruleUtils.getRuleOptions(type, obj);
|
||||
|
||||
expect(result).toEqual({
|
||||
defaultType: type,
|
||||
lineBreaks: false,
|
||||
pop: false,
|
||||
next: null,
|
||||
push: null,
|
||||
error: false,
|
||||
fallback: false,
|
||||
value: null,
|
||||
type: null,
|
||||
shouldThrow: false,
|
||||
match: ['abc'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error if type property is a string', () => {
|
||||
const type = 'content';
|
||||
const obj = { match: 'abc', type: 'invalid' };
|
||||
|
||||
expect(() => {
|
||||
ruleUtils.getRuleOptions(type, obj);
|
||||
}).toThrowError('type 属性不能为字符串!');
|
||||
});
|
||||
|
||||
it('should throw an error if match property includes include property', () => {
|
||||
const type = 'content';
|
||||
const obj = { match: 'abc', include: 'state' };
|
||||
|
||||
expect(() => {
|
||||
ruleUtils.getRuleOptions(type, obj);
|
||||
}).toThrowError('匹配规则不能包含状态!');
|
||||
});
|
||||
|
||||
it('should sort the match property correctly', () => {
|
||||
const type = 'content';
|
||||
const obj = { match: ['abc', /def/, 'ghi', /[0-9]+/, /^xyz$/] };
|
||||
|
||||
const result = ruleUtils.getRuleOptions(type, obj);
|
||||
|
||||
expect(result.match).toEqual(['abc', 'ghi', /def/, /[0-9]+/, /^xyz$/]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRulesByArray', () => {
|
||||
it('should return an empty array if the input array is empty', () => {
|
||||
const array = [];
|
||||
const result = ruleUtils.getRulesByArray(array);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should throw an error if a rule object does not have a type property', () => {
|
||||
const array = [{ match: 'abc' }, { type: 'content', match: 'def' }];
|
||||
expect(() => {
|
||||
ruleUtils.getRulesByArray(array);
|
||||
}).toThrowError('Rule 没有 type 属性');
|
||||
});
|
||||
|
||||
it('should handle multiple include properties', () => {
|
||||
const array = [
|
||||
{ include: [{ type: 'other1', match: 'abc' }] },
|
||||
{
|
||||
include: [
|
||||
{ type: 'other2', match: 'def' },
|
||||
{ type: 'other3', match: 'ghi' },
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = ruleUtils.getRulesByArray(array);
|
||||
expect(result).toEqual([
|
||||
{ include: { type: 'other1', match: 'abc' } },
|
||||
{ include: { type: 'other2', match: 'def' } },
|
||||
{ include: { type: 'other3', match: 'ghi' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRulesByObject', () => {
|
||||
it('should handle empty object correctly', () => {
|
||||
const object = {};
|
||||
|
||||
const result = ruleUtils.getRulesByObject(object);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle include property correctly', () => {
|
||||
const object = {
|
||||
include: ['rule1', 'rule2'],
|
||||
};
|
||||
|
||||
const result = ruleUtils.getRulesByObject(object);
|
||||
|
||||
expect(result).toEqual([{ include: 'rule1' }, { include: 'rule2' }]);
|
||||
});
|
||||
|
||||
it('should handle nested objects correctly', () => {
|
||||
const object = {
|
||||
rule1: {
|
||||
match: 'abc',
|
||||
next: 'rule2',
|
||||
fallback: true,
|
||||
},
|
||||
rule2: [/def/, { match: 'ghi', push: 'rule3' }],
|
||||
rule3: {
|
||||
match: 'jkl',
|
||||
error: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = ruleUtils.getRulesByObject(object);
|
||||
|
||||
const item1 = ruleUtils.getRuleOptions('rule1', { match: 'abc', next: 'rule2', fallback: true });
|
||||
const item2 = ruleUtils.getRuleOptions('rule2', [/def/]);
|
||||
delete item2[0];
|
||||
const item3 = ruleUtils.getRuleOptions('rule2', { match: 'ghi', push: 'rule3' });
|
||||
const item4 = ruleUtils.getRuleOptions('rule3', { match: 'jkl', error: true });
|
||||
|
||||
expect(result).toEqual([item1, item2, item3, item4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transferReg function', () => {
|
||||
it('should escape special characters in the input string', () => {
|
||||
const input = '[-/\\^$*+?.()|[\\]{}]';
|
||||
const expected = '\\[\\-\\/\\\\\\^\\$\\*\\+\\?\\.\\(\\)\\|\\[\\\\\\]\\{\\}\\]';
|
||||
|
||||
const result = ruleUtils.transferReg(input);
|
||||
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('should return the same string if no special characters are present', () => {
|
||||
const input = 'abcdefg';
|
||||
|
||||
const result = ruleUtils.transferReg(input);
|
||||
|
||||
expect(result).toBe(input);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "./build",
|
||||
"incremental": false,
|
||||
"sourceMap": true,
|
||||
"allowJs": true,
|
||||
// allowJs=true => tsc compile js as module, no type check
|
||||
"checkJs": false,
|
||||
// Disable ts error checking in js
|
||||
"strict": true,
|
||||
// js-ts mixed setting
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": false,
|
||||
// 等大部分js代码改成ts之后再启用.
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": false,
|
||||
"noImplicitThis": true,
|
||||
// "strictNullChecks": true,
|
||||
"module": "es2015",
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"moduleResolution": "node",
|
||||
"target": "es5",
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"allowUnreachableCode": true,
|
||||
"alwaysStrict": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"experimentalDecorators": true,
|
||||
"downlevelIteration": true,
|
||||
// 赋值为空数组使@types/node不会起作用
|
||||
"lib": [
|
||||
"dom",
|
||||
"esnext",
|
||||
"ES2015",
|
||||
"ES2016",
|
||||
"ES2017",
|
||||
"ES2018",
|
||||
"ES2019",
|
||||
"ES2020"
|
||||
],
|
||||
"strictNullChecks": true,
|
||||
"paths": {
|
||||
"react": [
|
||||
"node_modules/react"
|
||||
],
|
||||
"react-dom": [
|
||||
"node_modules/react-dom"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
"./src/format/**/*.ts",
|
||||
"./example/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib",
|
||||
"**/*.spec.ts",
|
||||
"dev"
|
||||
],
|
||||
"types": [
|
||||
"node",
|
||||
"jest",
|
||||
"@testing-library/jest-dom"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
const { resolve } = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
const entryPath = './example/index.tsx';
|
||||
|
||||
module.exports = {
|
||||
entry: resolve(__dirname, entryPath),
|
||||
output: {
|
||||
path: resolve(__dirname, './build'),
|
||||
filename: 'main.js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.([t|j]s)x?$/i,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env',
|
||||
[
|
||||
"@babel/preset-react",
|
||||
{
|
||||
"runtime": "automatic", // 新增
|
||||
"importSource": "@cloudsop/horizon" // 新增
|
||||
}
|
||||
],
|
||||
'@babel/preset-typescript'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
mode: isDevelopment ? 'development' : 'production',
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: resolve(__dirname, './example/index.html'),
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.jsx', '.ts', '.js', '.json'],
|
||||
},
|
||||
devServer: {
|
||||
https: false,
|
||||
host: 'localhost',
|
||||
port: '8080',
|
||||
open: true,
|
||||
hot: true,
|
||||
headers: {
|
||||
connection: 'keep-alive',
|
||||
},
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue