feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
1
frontend/packages/arch/i18n/.gitignore
vendored
Normal file
1
frontend/packages/arch/i18n/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
src/resource
|
||||
284
frontend/packages/arch/i18n/README.md
Normal file
284
frontend/packages/arch/i18n/README.md
Normal file
@@ -0,0 +1,284 @@
|
||||
# @coze-arch/i18n
|
||||
|
||||
A comprehensive internationalization (i18n) solution for the Coze platform, providing unified text localization and language management across all applications in the monorepo.
|
||||
|
||||
## Features
|
||||
|
||||
- **🌍 Multi-language Support**: Built-in support for English and Chinese (Simplified) with easy extensibility
|
||||
- **🎯 Type-safe Translations**: Full TypeScript support with auto-generated types for translation keys
|
||||
- **⚡ Multiple Integration Modes**: Support for both Eden.js projects and standalone applications
|
||||
- **🔌 Plugin Architecture**: Extensible plugin system with language detection and ICU formatting
|
||||
- **📱 React Integration**: Built-in React provider and context for seamless component integration
|
||||
- **🛡️ Fallback Handling**: Robust fallback mechanisms for missing translations
|
||||
- **🎨 Design System Integration**: Seamless integration with Coze Design components
|
||||
|
||||
## Get Started
|
||||
|
||||
### Installation
|
||||
|
||||
Add the package to your project:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@coze-arch/i18n": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
rush update
|
||||
```
|
||||
|
||||
### Basic Usage
|
||||
|
||||
#### For Eden.js Projects
|
||||
|
||||
Configure in your `edenx.config.ts`:
|
||||
|
||||
```typescript
|
||||
import { locale } from '@coze-arch/i18n/locales';
|
||||
|
||||
export default {
|
||||
intl: {
|
||||
mode: 'offline',
|
||||
clientOptions: {
|
||||
namespace: 'i18n',
|
||||
},
|
||||
intlOptions: {
|
||||
fallbackLng: 'en',
|
||||
ns: 'i18n',
|
||||
lng: 'en',
|
||||
resources: locale,
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Use in your components:
|
||||
|
||||
```typescript
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
function MyComponent() {
|
||||
const title = I18n.t('common.title');
|
||||
const greeting = I18n.t('common.greeting', { name: 'User' });
|
||||
|
||||
return <h1>{title}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
#### For Standalone Applications
|
||||
|
||||
Initialize before rendering:
|
||||
|
||||
```typescript
|
||||
import { initI18nInstance, I18n } from '@coze-arch/i18n/raw';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
initI18nInstance({
|
||||
lng: 'en',
|
||||
ns: 'i18n'
|
||||
}).then(() => {
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
root.render(<App />);
|
||||
});
|
||||
```
|
||||
|
||||
Use translations:
|
||||
|
||||
```typescript
|
||||
import { I18n } from '@coze-arch/i18n/raw';
|
||||
|
||||
function App() {
|
||||
const message = I18n.t('welcome.message');
|
||||
return <div>{message}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
#### React Provider Integration
|
||||
|
||||
```typescript
|
||||
import { I18nProvider } from '@coze-arch/i18n/i18n-provider';
|
||||
import { I18n } from '@coze-arch/i18n';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<I18nProvider i18n={I18n}>
|
||||
<YourComponents />
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core I18n Instance
|
||||
|
||||
#### `I18n.t(key, options?, fallback?)`
|
||||
|
||||
Translate a text key with optional interpolation and fallback.
|
||||
|
||||
```typescript
|
||||
// Basic translation
|
||||
I18n.t('common.save')
|
||||
|
||||
// With interpolation
|
||||
I18n.t('user.greeting', { name: 'John' })
|
||||
|
||||
// With fallback
|
||||
I18n.t('missing.key', {}, 'Default text')
|
||||
```
|
||||
|
||||
#### `I18n.setLang(language, callback?)`
|
||||
|
||||
Change the current language.
|
||||
|
||||
```typescript
|
||||
I18n.setLang('zh-CN', () => {
|
||||
console.log('Language changed');
|
||||
});
|
||||
```
|
||||
|
||||
#### `I18n.setLangWithPromise(language)`
|
||||
|
||||
Change language with Promise-based API.
|
||||
|
||||
```typescript
|
||||
await I18n.setLangWithPromise('en');
|
||||
```
|
||||
|
||||
#### `I18n.language`
|
||||
|
||||
Get the current language.
|
||||
|
||||
```typescript
|
||||
const currentLang = I18n.language; // 'en' | 'zh-CN'
|
||||
```
|
||||
|
||||
#### `I18n.getLanguages()`
|
||||
|
||||
Get available languages.
|
||||
|
||||
```typescript
|
||||
const languages = I18n.getLanguages(); // ['zh-CN', 'zh', 'en-US']
|
||||
```
|
||||
|
||||
#### `I18n.dir()`
|
||||
|
||||
Get text direction for current language.
|
||||
|
||||
```typescript
|
||||
const direction = I18n.dir(); // 'ltr' | 'rtl'
|
||||
```
|
||||
|
||||
### Initialization Functions
|
||||
|
||||
#### `initI18nInstance(config?)`
|
||||
|
||||
Initialize i18n for standalone applications.
|
||||
|
||||
```typescript
|
||||
interface I18nConfig {
|
||||
lng: 'en' | 'zh-CN';
|
||||
ns?: string;
|
||||
// Additional i18next options
|
||||
}
|
||||
|
||||
await initI18nInstance({
|
||||
lng: 'en',
|
||||
ns: 'i18n'
|
||||
});
|
||||
```
|
||||
|
||||
### React Components
|
||||
|
||||
#### `I18nProvider`
|
||||
|
||||
React context provider for i18n integration.
|
||||
|
||||
```typescript
|
||||
interface I18nProviderProps {
|
||||
children?: ReactNode;
|
||||
i18n: Intl;
|
||||
}
|
||||
```
|
||||
|
||||
### Type Utilities
|
||||
|
||||
#### `I18nKeysNoOptionsType`
|
||||
|
||||
Type for translation keys that don't require interpolation options.
|
||||
|
||||
#### `I18nKeysHasOptionsType`
|
||||
|
||||
Type for translation keys that require interpolation options.
|
||||
|
||||
## Development
|
||||
|
||||
### Updating Translations
|
||||
|
||||
Pull the latest translations from the remote source:
|
||||
|
||||
```bash
|
||||
rush pull-i18n
|
||||
```
|
||||
|
||||
This will update the locale files in `src/resource/locales/`.
|
||||
|
||||
### Adding New Locale Keys
|
||||
|
||||
1. Add new keys to the remote translation system
|
||||
2. Run `rush pull-i18n` to sync locally
|
||||
3. The TypeScript types will be automatically updated
|
||||
|
||||
### Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
rushx test
|
||||
```
|
||||
|
||||
Run tests with coverage:
|
||||
|
||||
```bash
|
||||
rushx test:cov
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts # Main export for Eden.js projects
|
||||
├── raw/ # Standalone application exports
|
||||
├── i18n-provider/ # React context provider
|
||||
├── intl/ # Core i18n implementation
|
||||
├── resource/ # Locale data and resources
|
||||
└── global.d.ts # Global type definitions
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Core Dependencies
|
||||
|
||||
- **i18next**: Core internationalization framework
|
||||
- **i18next-browser-languagedetector**: Automatic language detection in browsers
|
||||
- **i18next-icu**: ICU message formatting support
|
||||
- **@coze-studio/studio-i18n-resource-adapter**: Internal resource adapter for locale data
|
||||
- **@coze-arch/coze-design**: Design system integration
|
||||
|
||||
### Peer Dependencies
|
||||
|
||||
- **react**: React framework support
|
||||
- **react-dom**: React DOM rendering
|
||||
|
||||
## License
|
||||
|
||||
Internal package - ByteDance Ltd.
|
||||
|
||||
---
|
||||
|
||||
For questions or support, please contact the Coze Architecture team.
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { i18nContext } from '../../src/i18n-provider/context';
|
||||
|
||||
describe('i18n-provider/context', () => {
|
||||
it('should create a context with default values', () => {
|
||||
// 验证 i18nContext 是否被正确创建
|
||||
expect(i18nContext).toBeDefined();
|
||||
|
||||
// 获取默认值 - 使用类型断言访问内部属性
|
||||
// @ts-expect-error - 访问内部属性
|
||||
const defaultValue = i18nContext._currentValue;
|
||||
|
||||
// 验证默认值中的 i18n 对象是否存在
|
||||
expect(defaultValue.i18n).toBeDefined();
|
||||
|
||||
// 验证 t 函数是否存在
|
||||
expect(defaultValue.i18n.t).toBeDefined();
|
||||
expect(typeof defaultValue.i18n.t).toBe('function');
|
||||
|
||||
// 验证 t 函数的行为
|
||||
expect(defaultValue.i18n.t('test-key')).toBe('test-key');
|
||||
|
||||
// 验证 i18nContext 是一个对象
|
||||
expect(typeof i18nContext).toBe('object');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
import { I18nProvider } from '../../src/i18n-provider';
|
||||
|
||||
vi.mock('@coze-arch/coze-design/locales', () => ({
|
||||
CDLocaleProvider: vi.fn(() => ({
|
||||
render: vi.fn().mockImplementation(r => r),
|
||||
})),
|
||||
}));
|
||||
|
||||
describe('I18nProvider', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(I18nProvider).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render with default i18n when no i18n prop is provided', () => {
|
||||
const children = <div>Test</div>;
|
||||
const provider = new I18nProvider({ children });
|
||||
const result = provider.render().props.children;
|
||||
|
||||
// 验证渲染结果
|
||||
expect(result).toBeDefined();
|
||||
expect(result.props).toBeDefined();
|
||||
expect(result.props.value).toBeDefined();
|
||||
expect(result.props.value.i18n).toBeDefined();
|
||||
expect(typeof result.props.value.i18n.t).toBe('function');
|
||||
expect(result.props.children).toBe(children);
|
||||
|
||||
// 验证默认的 t 函数行为
|
||||
const defaultT = result.props.value.i18n.t;
|
||||
expect(defaultT('test-key')).toBe('test-key');
|
||||
});
|
||||
|
||||
it('should render with provided i18n', () => {
|
||||
const children = <div>Test</div>;
|
||||
const mockI18n = {
|
||||
t: vi.fn(key => `translated-${key}`),
|
||||
i18nInstance: {},
|
||||
init: vi.fn(),
|
||||
use: vi.fn(),
|
||||
language: 'zh-CN',
|
||||
languages: ['zh-CN', 'en-US'],
|
||||
messages: {},
|
||||
formatMessage: vi.fn(),
|
||||
};
|
||||
|
||||
const provider = new I18nProvider({ children, i18n: mockI18n as any });
|
||||
const result = provider.render().props.children;
|
||||
|
||||
// 验证渲染结果
|
||||
expect(result).toBeDefined();
|
||||
expect(result.props).toBeDefined();
|
||||
expect(result.props.value).toBeDefined();
|
||||
expect(result.props.value.i18n).toBe(mockI18n);
|
||||
expect(result.props.children).toBe(children);
|
||||
|
||||
// 验证使用了提供的 i18n
|
||||
const key = 'test-key';
|
||||
result.props.value.i18n.t(key);
|
||||
expect(mockI18n.t).toHaveBeenCalledWith(key);
|
||||
expect(mockI18n.t(key)).toBe('translated-test-key');
|
||||
});
|
||||
});
|
||||
126
frontend/packages/arch/i18n/__tests__/i18n.test.tsx
Normal file
126
frontend/packages/arch/i18n/__tests__/i18n.test.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest';
|
||||
|
||||
import { I18n, getUnReactiveLanguage } from '../src/index';
|
||||
|
||||
describe('I18n', () => {
|
||||
beforeAll(() => {
|
||||
I18n.init({
|
||||
lng: 'en',
|
||||
fallbackLng: 'en',
|
||||
resources: {
|
||||
en: { i18n: { test: 'Test', 'test-key': 'test-key-value' } },
|
||||
'zh-CN': { i18n: { test: '测试' } },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should have basic methods', () => {
|
||||
expect(I18n).toBeDefined();
|
||||
expect(I18n.language).toBe('en');
|
||||
expect(I18n.t).toBeDefined();
|
||||
expect(typeof I18n.t).toBe('function');
|
||||
});
|
||||
|
||||
it('should get language', () => {
|
||||
expect(getUnReactiveLanguage()).toBe('en');
|
||||
});
|
||||
|
||||
it('should call init method', () => {
|
||||
const initSpy = vi.spyOn(I18n, 'init');
|
||||
const config = { lng: 'en' };
|
||||
const callback = vi.fn();
|
||||
|
||||
I18n.init(config, callback);
|
||||
|
||||
expect(initSpy).toHaveBeenCalledWith(config, callback);
|
||||
});
|
||||
|
||||
it('should call use method', () => {
|
||||
const useSpy = vi.spyOn(I18n, 'use');
|
||||
const plugin = {};
|
||||
|
||||
I18n.use(plugin);
|
||||
|
||||
expect(useSpy).toHaveBeenCalledWith(plugin);
|
||||
});
|
||||
|
||||
it('should call setLang method', () => {
|
||||
const setLangSpy = vi.spyOn(I18n, 'setLang');
|
||||
const lang = 'zh-CN';
|
||||
const callback = vi.fn();
|
||||
|
||||
I18n.setLang(lang, callback);
|
||||
|
||||
expect(setLangSpy).toHaveBeenCalledWith(lang, callback);
|
||||
});
|
||||
|
||||
it('should call getLanguages method', () => {
|
||||
const getLanguagesSpy = vi.spyOn(I18n, 'getLanguages');
|
||||
|
||||
const result = I18n.getLanguages();
|
||||
|
||||
expect(getLanguagesSpy).toHaveBeenCalled();
|
||||
expect(result).toEqual(['zh-CN', 'zh', 'en-US']);
|
||||
});
|
||||
|
||||
it('should call dir method', () => {
|
||||
const dirSpy = vi.spyOn(I18n, 'dir');
|
||||
|
||||
const result = I18n.dir();
|
||||
|
||||
expect(dirSpy).toHaveBeenCalled();
|
||||
expect(result).toBe('ltr');
|
||||
});
|
||||
|
||||
it('should call addResourceBundle method', () => {
|
||||
const addResourceBundleSpy = vi.spyOn(I18n, 'addResourceBundle');
|
||||
const lng = 'en';
|
||||
const ns = 'test';
|
||||
const resources = { key: 'value' };
|
||||
const deep = true;
|
||||
const overwrite = false;
|
||||
|
||||
I18n.addResourceBundle(lng, ns, resources, deep, overwrite);
|
||||
|
||||
expect(addResourceBundleSpy).toHaveBeenCalledWith(
|
||||
lng,
|
||||
ns,
|
||||
resources,
|
||||
deep,
|
||||
overwrite,
|
||||
);
|
||||
});
|
||||
|
||||
it('should call t method', () => {
|
||||
I18n.setLang('en');
|
||||
const tSpy = vi.spyOn(I18n, 't');
|
||||
const key = 'test-key';
|
||||
const options = { ns: 'i18n' };
|
||||
const fallbackText = 'fallback';
|
||||
|
||||
const result = I18n.t(key as any, options, fallbackText);
|
||||
|
||||
expect(tSpy).toHaveBeenCalledWith(key, options, fallbackText);
|
||||
expect(result).toBe('test-key-value');
|
||||
});
|
||||
});
|
||||
109
frontend/packages/arch/i18n/__tests__/raw/index.test.ts
Normal file
109
frontend/packages/arch/i18n/__tests__/raw/index.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
import { I18n, initI18nInstance } from '../../src/raw';
|
||||
|
||||
// 模拟本地化资源
|
||||
vi.mock('../../src/resource.ts', () => ({
|
||||
default: {
|
||||
en: { i18n: { test: 'Test' } },
|
||||
'zh-CN': { i18n: { test: '测试' } },
|
||||
},
|
||||
}));
|
||||
|
||||
describe('raw/index', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should export I18n', () => {
|
||||
expect(I18n).toBeDefined();
|
||||
});
|
||||
|
||||
it('should initialize I18n with default config', async () => {
|
||||
await initI18nInstance();
|
||||
expect(I18n.plugins).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ type: 'languageDetector' }),
|
||||
]),
|
||||
);
|
||||
expect(I18n.i18nInstance.config).toEqual(
|
||||
expect.objectContaining({
|
||||
fallbackLng: 'en',
|
||||
lng: 'en',
|
||||
ns: 'i18n',
|
||||
defaultNS: 'i18n',
|
||||
resources: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize I18n with custom config', async () => {
|
||||
const customConfig = {
|
||||
lng: 'zh-CN' as const,
|
||||
ns: 'custom',
|
||||
debug: true,
|
||||
};
|
||||
|
||||
await initI18nInstance(customConfig);
|
||||
|
||||
expect(I18n.plugins).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ type: 'languageDetector' }),
|
||||
]),
|
||||
);
|
||||
expect(I18n.i18nInstance.config).toEqual(
|
||||
expect.objectContaining({
|
||||
fallbackLng: 'zh-CN',
|
||||
lng: 'zh-CN',
|
||||
ns: 'custom',
|
||||
defaultNS: 'custom',
|
||||
debug: true,
|
||||
resources: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
expect(I18n.t('test', { ns: 'i18n' })).toEqual('测试');
|
||||
});
|
||||
it('should call addResourceBundle method', () => {
|
||||
const lng = 'en';
|
||||
const ns = 'test';
|
||||
const resources = {
|
||||
en: { i18n: { test: 'Test' } },
|
||||
};
|
||||
const deep = true;
|
||||
const overwrite = false;
|
||||
|
||||
I18n.addResourceBundle(lng, ns, resources, deep, overwrite);
|
||||
expect(I18n.t('test', { ns: 'i18n' })).toEqual('测试');
|
||||
expect(I18n.t('unknown')).toEqual('unknown');
|
||||
});
|
||||
it('should get languages', () => {
|
||||
let fireCallback = false;
|
||||
I18n.setLang('en', () => {
|
||||
fireCallback = true;
|
||||
});
|
||||
expect(I18n.t('test', { ns: 'i18n' })).toEqual('Test');
|
||||
expect(fireCallback).toEqual(true);
|
||||
|
||||
I18n.setLangWithPromise('zh-CN').then(() => {
|
||||
expect(I18n.t('test', { ns: 'i18n' })).toEqual('测试');
|
||||
});
|
||||
expect(I18n.dir('en', { ns: 'i18n' })).toEqual('ltr');
|
||||
expect(I18n.language).toEqual('zh-CN');
|
||||
});
|
||||
});
|
||||
12
frontend/packages/arch/i18n/config/rush-project.json
Normal file
12
frontend/packages/arch/i18n/config/rush-project.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"operationSettings": [
|
||||
{
|
||||
"operationName": "test:cov",
|
||||
"outputFolderNames": ["coverage"]
|
||||
},
|
||||
{
|
||||
"operationName": "ts-check",
|
||||
"outputFolderNames": ["./dist"]
|
||||
}
|
||||
]
|
||||
}
|
||||
5
frontend/packages/arch/i18n/config/rushx-config.json
Normal file
5
frontend/packages/arch/i18n/config/rushx-config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"codecov": {
|
||||
"incrementCoverage": 80
|
||||
}
|
||||
}
|
||||
7
frontend/packages/arch/i18n/eslint.config.js
Normal file
7
frontend/packages/arch/i18n/eslint.config.js
Normal file
@@ -0,0 +1,7 @@
|
||||
const { defineConfig } = require('@coze-arch/eslint-config');
|
||||
|
||||
module.exports = defineConfig({
|
||||
packageRoot: __dirname,
|
||||
preset: 'web',
|
||||
rules: {},
|
||||
});
|
||||
64
frontend/packages/arch/i18n/package.json
Normal file
64
frontend/packages/arch/i18n/package.json
Normal file
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@coze-arch/i18n",
|
||||
"version": "0.0.1",
|
||||
"author": "fanwenjie.fe@bytedance.com",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./raw": "./src/raw/index.ts",
|
||||
"./locales": "./src/resource.ts",
|
||||
"./i18n-provider": "./src/i18n-provider/index.tsx",
|
||||
"./intl": "./src/intl/index.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"raw": [
|
||||
"./src/raw/index.ts"
|
||||
],
|
||||
"locales": [
|
||||
"./src/resource.ts"
|
||||
],
|
||||
"i18n-provider": [
|
||||
"./src/i18n-provider/index.tsx"
|
||||
],
|
||||
"intl": [
|
||||
"./src/intl/index.ts"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "exit 0",
|
||||
"lint": "eslint ./ --cache",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:cov": "npm run test -- --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@coze-arch/coze-design": "0.0.6-alpha.346d77",
|
||||
"@coze-studio/studio-i18n-resource-adapter": "workspace:*",
|
||||
"i18next": ">= 19.0.0",
|
||||
"i18next-browser-languagedetector": "8.0.4",
|
||||
"i18next-icu": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.2",
|
||||
"@coze-arch/bot-typings": "workspace:*",
|
||||
"@coze-arch/eslint-config": "workspace:*",
|
||||
"@coze-arch/ts-config": "workspace:*",
|
||||
"@coze-arch/vitest-config": "workspace:*",
|
||||
"@rsbuild/core": "1.1.13",
|
||||
"@types/node": "^18",
|
||||
"@types/react": "18.2.37",
|
||||
"@types/react-dom": "18.2.15",
|
||||
"@vitest/coverage-v8": "~3.0.5",
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0",
|
||||
"react-is": ">= 16.8.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vitest": "~3.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "~18.2.0",
|
||||
"react-dom": "~18.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
17
frontend/packages/arch/i18n/src/global.d.ts
vendored
Normal file
17
frontend/packages/arch/i18n/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types='@coze-arch/bot-typings' />
|
||||
29
frontend/packages/arch/i18n/src/i18n-provider/context.tsx
Normal file
29
frontend/packages/arch/i18n/src/i18n-provider/context.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { type Intl } from '../intl';
|
||||
|
||||
interface I18nContext {
|
||||
i18n: Intl;
|
||||
}
|
||||
const i18nContext = React.createContext<I18nContext>({
|
||||
i18n: {
|
||||
t: k => k,
|
||||
} as unknown as Intl,
|
||||
});
|
||||
export { i18nContext, type I18nContext };
|
||||
52
frontend/packages/arch/i18n/src/i18n-provider/index.tsx
Normal file
52
frontend/packages/arch/i18n/src/i18n-provider/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Component, type ReactNode } from 'react';
|
||||
|
||||
import { CDLocaleProvider } from '@coze-arch/coze-design/locales';
|
||||
|
||||
import { type Intl } from '../intl';
|
||||
import { i18nContext, type I18nContext } from './context';
|
||||
|
||||
export { i18nContext, type I18nContext };
|
||||
|
||||
export interface I18nProviderProps {
|
||||
children?: ReactNode;
|
||||
i18n: Intl;
|
||||
}
|
||||
|
||||
export class I18nProvider extends Component<I18nProviderProps> {
|
||||
constructor(props: I18nProviderProps) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
i18n = {
|
||||
t: (k: string) => k,
|
||||
},
|
||||
} = this.props;
|
||||
return (
|
||||
<CDLocaleProvider i18n={i18n}>
|
||||
<i18nContext.Provider value={{ i18n: i18n as Intl }}>
|
||||
{children}
|
||||
</i18nContext.Provider>
|
||||
</CDLocaleProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
116
frontend/packages/arch/i18n/src/index.ts
Normal file
116
frontend/packages/arch/i18n/src/index.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable max-params */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type {
|
||||
LocaleData,
|
||||
I18nOptionsMap,
|
||||
I18nKeysHasOptionsType,
|
||||
I18nKeysNoOptionsType,
|
||||
} from '@coze-studio/studio-i18n-resource-adapter';
|
||||
|
||||
import {
|
||||
type Intl,
|
||||
type I18nCore,
|
||||
type IIntlInitOptions,
|
||||
I18n as _I18n,
|
||||
} from './intl';
|
||||
|
||||
type Callback = Parameters<(typeof _I18n)['init']>[1];
|
||||
type FallbackLng = ReturnType<(typeof _I18n)['getLanguages']>;
|
||||
type IntlModule = Parameters<(typeof _I18n)['use']>[0];
|
||||
type InitReturnType = ReturnType<(typeof _I18n)['init']>;
|
||||
type I18nOptions<K extends LocaleData> = K extends keyof I18nOptionsMap
|
||||
? I18nOptionsMap[K]
|
||||
: never;
|
||||
|
||||
// 这里导出的 const I18n = new FlowIntl() 与 '@edenx/plugin-starling-intl/runtime' 中的 I18n 功能等价
|
||||
// 其实就是对 '@edenx/plugin-starling-intl/runtime' 中的 I18n 进行了一层封装,目的是为了后续进一步灵活的定义I18n.t() 的参数类型。
|
||||
// 这里的 I18n.t() 的参数类型是通过泛型 LocaleData 来定义的,而 '@edenx/plugin-starling-intl/runtime' 中的 I18n.t() 的参数类型是通过泛型 string 来定义的。
|
||||
class FlowIntl {
|
||||
plugins: any[] = [];
|
||||
public i18nInstance: I18nCore;
|
||||
constructor() {
|
||||
this.i18nInstance = _I18n.i18nInstance;
|
||||
}
|
||||
|
||||
init(config: IIntlInitOptions, callback?: Callback): InitReturnType {
|
||||
return _I18n.init(config, callback);
|
||||
}
|
||||
|
||||
use(plugin: IntlModule): Intl {
|
||||
return _I18n.use(plugin);
|
||||
}
|
||||
|
||||
get language(): string {
|
||||
return _I18n.language;
|
||||
}
|
||||
|
||||
setLangWithPromise(lng: string) {
|
||||
return this.i18nInstance.changeLanguageWithPromise(lng);
|
||||
}
|
||||
|
||||
setLang(lng: string, callback?: Callback): void {
|
||||
return _I18n.setLang(lng, callback);
|
||||
}
|
||||
|
||||
getLanguages(): FallbackLng {
|
||||
return _I18n.getLanguages();
|
||||
}
|
||||
|
||||
dir(): 'ltr' | 'rtl' {
|
||||
return _I18n.dir();
|
||||
}
|
||||
|
||||
addResourceBundle(
|
||||
lng: string,
|
||||
ns: string,
|
||||
resources: any,
|
||||
deep?: boolean,
|
||||
overwrite?: boolean,
|
||||
) {
|
||||
return _I18n.addResourceBundle(lng, ns, resources, deep, overwrite);
|
||||
}
|
||||
|
||||
t<K extends I18nKeysNoOptionsType>(
|
||||
keys: K,
|
||||
// 这里如果用 never 的话,导致存量代码第二个参数是 `{}` 的时候会报错,所以这里用 Record<string, unknown> 代替
|
||||
// 后续的做法是:用 sg 把存量的代码都修复了之后,这里再改成 never 类型,从而保证未来新增的代码,都是有类型检查的。
|
||||
// 记得改动的时候 #87 行也要一起修改
|
||||
options?: Record<string, unknown>,
|
||||
fallbackText?: string,
|
||||
): string;
|
||||
t<K extends I18nKeysHasOptionsType>(
|
||||
keys: K,
|
||||
options: I18nOptions<K>,
|
||||
fallbackText?: string,
|
||||
): string;
|
||||
t<K extends LocaleData>(
|
||||
keys: K,
|
||||
options?: I18nOptions<K> | Record<string, unknown>,
|
||||
fallbackText?: string,
|
||||
): string {
|
||||
// tecvan: fixme, hard to understand why this happens
|
||||
return _I18n.t(keys, options, fallbackText);
|
||||
}
|
||||
}
|
||||
|
||||
export const getUnReactiveLanguage = () => _I18n.language;
|
||||
export const I18n = new FlowIntl();
|
||||
|
||||
export { type I18nKeysNoOptionsType, type I18nKeysHasOptionsType };
|
||||
134
frontend/packages/arch/i18n/src/intl/i18n-impl.ts
Normal file
134
frontend/packages/arch/i18n/src/intl/i18n-impl.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-params */
|
||||
/* eslint-disable @typescript-eslint/no-this-alias */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import type { Callback, TFunction, InitOptions, FallbackLng } from 'i18next';
|
||||
|
||||
import type { StringMap, TFunctionKeys } from './types';
|
||||
import I18next, { formatLang, isTypes, LANGUAGE_TRANSFORMER } from './i18n';
|
||||
|
||||
export interface IntlConstructorOptions {
|
||||
i18nInstance?: I18next;
|
||||
}
|
||||
let intlInstance: any = null;
|
||||
/**
|
||||
* I18n实例
|
||||
* 自定义配置
|
||||
*/
|
||||
class Intl {
|
||||
plugins: any[];
|
||||
i18nInstance: I18next;
|
||||
constructor(opts?: IntlConstructorOptions) {
|
||||
this.plugins = [];
|
||||
this.i18nInstance = opts?.i18nInstance ?? new I18next();
|
||||
}
|
||||
/**
|
||||
* i18n 没有定义类型,这里声明 any
|
||||
*/
|
||||
use(plugin: any) {
|
||||
if (!this.plugins.includes(plugin)) {
|
||||
this.plugins.push(plugin);
|
||||
return this;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
async init(
|
||||
config: InitOptions,
|
||||
initCallback?: Callback,
|
||||
): Promise<{ err: Error; t: TFunction }> {
|
||||
this.i18nInstance._handleConfigs(config as any);
|
||||
this.i18nInstance._handlePlugins(this.plugins);
|
||||
|
||||
try {
|
||||
const { err, t } = await this.i18nInstance.createInstance();
|
||||
|
||||
typeof initCallback === 'function' && initCallback(err, t);
|
||||
return { err, t };
|
||||
} catch (err) {
|
||||
console.log('debugger error', err);
|
||||
return {
|
||||
err,
|
||||
t: ((key: string) => key) as TFunction<'translation', undefined>,
|
||||
};
|
||||
}
|
||||
}
|
||||
get language() {
|
||||
return (this.i18nInstance || {}).language;
|
||||
}
|
||||
getLanguages(): FallbackLng {
|
||||
return this.i18nInstance.getLanguages() || [];
|
||||
}
|
||||
setLang(lng: string, callback?: Callback) {
|
||||
const formatLng = formatLang(
|
||||
lng,
|
||||
this.plugins.filter(isTypes(LANGUAGE_TRANSFORMER)),
|
||||
);
|
||||
this.i18nInstance.changeLanguage(formatLng, callback);
|
||||
}
|
||||
setLangWithPromise(lng: string) {
|
||||
const formatLng = formatLang(
|
||||
lng,
|
||||
this.plugins.filter(isTypes(LANGUAGE_TRANSFORMER)),
|
||||
);
|
||||
return this.i18nInstance.changeLanguageWithPromise(formatLng);
|
||||
}
|
||||
dir(lng: string) {
|
||||
return this.i18nInstance.getDir(lng);
|
||||
}
|
||||
addResourceBundle(
|
||||
lng: string,
|
||||
ns: string,
|
||||
resources: any,
|
||||
deep?: boolean,
|
||||
overwrite?: boolean,
|
||||
) {
|
||||
// to to something validate
|
||||
return this.i18nInstance.addResourceBundle(
|
||||
lng,
|
||||
ns,
|
||||
resources,
|
||||
deep,
|
||||
overwrite,
|
||||
);
|
||||
}
|
||||
t<
|
||||
TKeys extends TFunctionKeys = string,
|
||||
TInterpolationMap extends object = StringMap,
|
||||
>(keys: TKeys | TKeys[], options?: TInterpolationMap, fallbackText?: string) {
|
||||
let that: any = null;
|
||||
if (typeof this === 'undefined') {
|
||||
that = intlInstance;
|
||||
} else {
|
||||
that = this;
|
||||
}
|
||||
if (!that.i18nInstance || !that.i18nInstance.init) {
|
||||
return fallbackText ?? (Array.isArray(keys) ? keys[0] : keys);
|
||||
}
|
||||
// 有人给 key 传空字符串?
|
||||
if (!keys || (typeof keys === 'string' && !keys.trim())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return that.i18nInstance.t(keys, options, fallbackText);
|
||||
}
|
||||
}
|
||||
|
||||
intlInstance = new Intl();
|
||||
|
||||
export default Intl;
|
||||
export { intlInstance as IntlInstance };
|
||||
259
frontend/packages/arch/i18n/src/intl/i18n.ts
Normal file
259
frontend/packages/arch/i18n/src/intl/i18n.ts
Normal file
@@ -0,0 +1,259 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-params */
|
||||
/* eslint-disable no-empty */
|
||||
/* eslint-disable @coze-arch/no-empty-catch */
|
||||
/* eslint-disable @coze-arch/use-error-in-catch */
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
import ICU from 'i18next-icu';
|
||||
import i18next, {
|
||||
type TOptions,
|
||||
type Callback,
|
||||
type FallbackLng,
|
||||
type InitOptions,
|
||||
type Module,
|
||||
type TFunction,
|
||||
type i18n,
|
||||
} from 'i18next';
|
||||
|
||||
import {
|
||||
type StringMap,
|
||||
type TFunctionKeys,
|
||||
type TFunctionResult,
|
||||
} from './types';
|
||||
|
||||
export const LANGUAGE_TRANSFORMER = 'languageTransformer';
|
||||
|
||||
export function isTypes(type) {
|
||||
return itm => itm.type === type;
|
||||
}
|
||||
|
||||
export function formatLang(lng, plugins) {
|
||||
let fl = lng;
|
||||
(plugins || []).map(plugin => {
|
||||
fl = plugin.process(lng) || fl;
|
||||
});
|
||||
return fl;
|
||||
}
|
||||
|
||||
const defaultFallbackLanguage = 'zh-CN';
|
||||
const defaultConfig = {
|
||||
lng: defaultFallbackLanguage, // 如果使用了 Language Detector,i18next 底层 lng 的权重是大于插件的
|
||||
fallbackLng: ['en-US'],
|
||||
inContext: true,
|
||||
};
|
||||
// 默认开启ICU插值解析
|
||||
|
||||
/**
|
||||
* I18n内核
|
||||
* 安全校验
|
||||
*/
|
||||
export default class I18next {
|
||||
instance: i18n;
|
||||
config?: InitOptions & {
|
||||
lng?: string;
|
||||
fallbackLng?: string[];
|
||||
[key: string]: any;
|
||||
};
|
||||
plugins?: any[];
|
||||
languages?: FallbackLng;
|
||||
init?: boolean;
|
||||
userLng?: string | null;
|
||||
|
||||
private _waitingToAddResourceBundle: [
|
||||
string,
|
||||
string,
|
||||
any,
|
||||
boolean,
|
||||
boolean,
|
||||
][] = [];
|
||||
|
||||
_handlePlugins(plugins?: any[]) {
|
||||
this.plugins = plugins;
|
||||
}
|
||||
|
||||
_handleConfigs(config?: InitOptions) {
|
||||
this.userLng = config?.lng || null; // 用户自己设定的 lng
|
||||
|
||||
this.config = Object.assign({}, defaultConfig, config || {});
|
||||
}
|
||||
|
||||
constructor(
|
||||
config?: InitOptions & { copiedI18nextInstance?: any },
|
||||
plugins?: any[],
|
||||
) {
|
||||
if (config?.copiedI18nextInstance) {
|
||||
// just clone instance
|
||||
this.instance = config.copiedI18nextInstance;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this._handlePlugins(plugins);
|
||||
this._handleConfigs(config);
|
||||
this.instance = i18next.createInstance();
|
||||
this.instance.use(ICU);
|
||||
this.instance.isInitialized = false;
|
||||
}
|
||||
get language() {
|
||||
return (this.instance || {}).language;
|
||||
}
|
||||
createInstance(): Promise<{ err: Error; t: TFunction }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.plugins?.map(p => {
|
||||
this.instance.use(p as Module);
|
||||
});
|
||||
|
||||
const { config } = this;
|
||||
|
||||
this.config!.formats = Object.assign({}, this.config!.formats);
|
||||
const formatLng = formatLang(
|
||||
config!.lng,
|
||||
this.plugins?.filter(isTypes(LANGUAGE_TRANSFORMER)),
|
||||
);
|
||||
this.instance.init(
|
||||
{
|
||||
...config,
|
||||
lng: formatLng,
|
||||
i18nFormat: {
|
||||
...(config!.i18nFormat || {}),
|
||||
formats: this.config!.formats,
|
||||
},
|
||||
},
|
||||
(err, t) => {
|
||||
// 初始化好了
|
||||
|
||||
try {
|
||||
// 把等待添加的东西都加进去
|
||||
for (const item of this._waitingToAddResourceBundle) {
|
||||
this.instance.addResourceBundle(...item);
|
||||
}
|
||||
this._waitingToAddResourceBundle = [];
|
||||
} catch (_err) {}
|
||||
|
||||
if (!err) {
|
||||
this._updateLanguages();
|
||||
resolve({
|
||||
t,
|
||||
err,
|
||||
});
|
||||
}
|
||||
this.init = true;
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject({
|
||||
t,
|
||||
err,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
getLanguages() {
|
||||
return this.languages;
|
||||
}
|
||||
addResourceBundle(
|
||||
lng: string,
|
||||
ns: string,
|
||||
resources: any,
|
||||
deep?: boolean,
|
||||
overwrite?: boolean,
|
||||
) {
|
||||
if (this.instance.isInitialized) {
|
||||
return this.instance.addResourceBundle(
|
||||
lng,
|
||||
ns,
|
||||
resources,
|
||||
deep,
|
||||
overwrite,
|
||||
);
|
||||
}
|
||||
// 还没初始化好
|
||||
this._waitingToAddResourceBundle.push([
|
||||
lng,
|
||||
ns,
|
||||
resources,
|
||||
!!deep,
|
||||
!!overwrite,
|
||||
]);
|
||||
return this.instance;
|
||||
}
|
||||
_updateLanguages() {
|
||||
this.languages = this.instance
|
||||
? (Array.from(
|
||||
new Set([this.instance.language, ...this.instance.languages]),
|
||||
) as FallbackLng)
|
||||
: (null as unknown as FallbackLng);
|
||||
}
|
||||
changeLanguage(lng: string, callback?: Callback) {
|
||||
this.config!.lng = lng;
|
||||
this.instance.changeLanguage(lng, (err, t) => {
|
||||
if (!err) {
|
||||
this._updateLanguages();
|
||||
}
|
||||
callback && callback(err, t);
|
||||
});
|
||||
}
|
||||
changeLanguageWithPromise(lng: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.config!.lng = lng;
|
||||
this.instance.changeLanguage(lng, (err, t) => {
|
||||
if (err) {
|
||||
// eslint-disable-next-line prefer-promise-reject-errors
|
||||
reject({
|
||||
err,
|
||||
t,
|
||||
});
|
||||
}
|
||||
this._updateLanguages();
|
||||
resolve({ err, t });
|
||||
});
|
||||
});
|
||||
}
|
||||
getDir(lng: string) {
|
||||
return this.instance.dir(lng);
|
||||
}
|
||||
t<
|
||||
TResult extends TFunctionResult = string,
|
||||
TKeys extends TFunctionKeys = string,
|
||||
TInterpolationMap extends object = StringMap,
|
||||
>(
|
||||
keys: TKeys | TKeys[],
|
||||
options?: TOptions<TInterpolationMap> | string,
|
||||
fallbackText?: string,
|
||||
): TResult {
|
||||
const separatorMock = Array.isArray(keys)
|
||||
? Array.from(keys)
|
||||
.map(() => ' ')
|
||||
.join('')
|
||||
: Array(keys.length).fill(' ');
|
||||
|
||||
// fixed: 去除默认lngs,有lngs i18next就会忽略lng
|
||||
const opt: Record<string, any> = Object.assign(
|
||||
{ keySeparator: separatorMock, nsSeparator: separatorMock },
|
||||
options,
|
||||
);
|
||||
|
||||
return this.instance.t(
|
||||
keys as string,
|
||||
fallbackText as string,
|
||||
opt,
|
||||
) as TResult;
|
||||
}
|
||||
}
|
||||
27
frontend/packages/arch/i18n/src/intl/index.ts
Normal file
27
frontend/packages/arch/i18n/src/intl/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { type IIntlInitOptions, IntlModuleType, IntlModule } from './types';
|
||||
import Intl, { IntlInstance } from './i18n-impl';
|
||||
|
||||
export { default as I18nCore } from './i18n';
|
||||
|
||||
const i18n = IntlInstance;
|
||||
i18n.t = i18n.t.bind(i18n);
|
||||
const i18nConstructor = Intl;
|
||||
|
||||
export default i18n;
|
||||
export { i18n as I18n, Intl, i18nConstructor as I18nConstructor };
|
||||
62
frontend/packages/arch/i18n/src/intl/types.ts
Normal file
62
frontend/packages/arch/i18n/src/intl/types.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/* eslint-disable @stylistic/ts/comma-dangle */
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
/* eslint-disable prettier/prettier */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-invalid-void-type */
|
||||
import { type InitOptions } from 'i18next';
|
||||
|
||||
/**
|
||||
* 初始化 Intl 实例配置参数
|
||||
*/
|
||||
export interface IIntlInitOptions
|
||||
extends Omit<InitOptions, 'missingInterpolationHandler'> {
|
||||
/**
|
||||
* t 方法是否开启第三个参数兜底
|
||||
* @default true
|
||||
*/
|
||||
thirdParamFallback?: boolean;
|
||||
|
||||
/**
|
||||
* 忽略所有控制台输出,不建议设置为 true
|
||||
* @default false
|
||||
*/
|
||||
ignoreWarning?: boolean;
|
||||
}
|
||||
|
||||
export enum IntlModuleType {
|
||||
intl3rdParty = 'intl3rdParty',
|
||||
backend = 'backend',
|
||||
logger = 'logger',
|
||||
languageDetector = 'languageDetector',
|
||||
postProcessor = 'postProcessor',
|
||||
i18nFormat = 'i18nFormat',
|
||||
'3rdParty' = '3rdParty'
|
||||
}
|
||||
|
||||
export interface IntlModule<T extends keyof typeof IntlModuleType = keyof typeof IntlModuleType> {
|
||||
type: T
|
||||
name?: string
|
||||
init?: (i18n: any) => void | Promise<any>
|
||||
}
|
||||
|
||||
export type TFunctionKeys = string | TemplateStringsArray;
|
||||
|
||||
export type TFunctionResult = string | object | Array<string | object> | undefined | null;
|
||||
|
||||
export interface StringMap { [key: string]: any }
|
||||
67
frontend/packages/arch/i18n/src/raw/index.ts
Normal file
67
frontend/packages/arch/i18n/src/raw/index.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
|
||||
import locale from '../resource';
|
||||
export {
|
||||
type I18nKeysNoOptionsType,
|
||||
type I18nKeysHasOptionsType,
|
||||
} from '@coze-studio/studio-i18n-resource-adapter';
|
||||
import { I18n } from '../intl';
|
||||
|
||||
interface I18nConfig extends Record<string, unknown> {
|
||||
lng: 'en' | 'zh-CN';
|
||||
ns?: string;
|
||||
}
|
||||
export function initI18nInstance(config?: I18nConfig) {
|
||||
const { lng = 'en', ns, ...restConfig } = config || {};
|
||||
return new Promise(resolve => {
|
||||
I18n.use(LanguageDetector);
|
||||
I18n.init(
|
||||
{
|
||||
detection: {
|
||||
order: [
|
||||
'querystring',
|
||||
'cookie',
|
||||
'localStorage',
|
||||
'navigator',
|
||||
'htmlTag',
|
||||
],
|
||||
lookupQuerystring: 'lng',
|
||||
lookupCookie: 'i18next',
|
||||
lookupLocalStorage: 'i18next',
|
||||
fallback: 'zh-CN',
|
||||
caches: ['cookie'],
|
||||
mute: false,
|
||||
},
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
keySeparator: false,
|
||||
fallbackLng: lng,
|
||||
lng,
|
||||
ns: ns || 'i18n',
|
||||
defaultNS: ns || 'i18n',
|
||||
resources: locale,
|
||||
...(restConfig ?? {}),
|
||||
},
|
||||
resolve,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export { I18n };
|
||||
19
frontend/packages/arch/i18n/src/resource.ts
Normal file
19
frontend/packages/arch/i18n/src/resource.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defaultConfig } from '@coze-studio/studio-i18n-resource-adapter';
|
||||
|
||||
export default defaultConfig;
|
||||
29
frontend/packages/arch/i18n/tsconfig.build.json
Normal file
29
frontend/packages/arch/i18n/tsconfig.build.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"extends": "@coze-arch/ts-config/tsconfig.node.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"useUnknownInCatchVariables": false,
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"tsBuildInfoFile": "dist/tsconfig.build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src", "src/**/*.json"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../bot-typings/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/eslint-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/ts-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../../../config/vitest-config/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../resources/studio-i18n-resource/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
15
frontend/packages/arch/i18n/tsconfig.json
Normal file
15
frontend/packages/arch/i18n/tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.misc.json"
|
||||
}
|
||||
],
|
||||
"exclude": ["**/*"]
|
||||
}
|
||||
17
frontend/packages/arch/i18n/tsconfig.misc.json
Normal file
17
frontend/packages/arch/i18n/tsconfig.misc.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@coze-arch/ts-config/tsconfig.node.json",
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"include": ["__tests__", "vitest.config.ts"],
|
||||
"exclude": ["./dist"],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.build.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"useUnknownInCatchVariables": false
|
||||
}
|
||||
}
|
||||
28
frontend/packages/arch/i18n/vitest.config.ts
Normal file
28
frontend/packages/arch/i18n/vitest.config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2025 coze-dev Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from '@coze-arch/vitest-config';
|
||||
|
||||
export default defineConfig({
|
||||
dirname: __dirname,
|
||||
preset: 'web',
|
||||
test: {
|
||||
coverage: {
|
||||
all: true,
|
||||
exclude: ['starling.config.js', 'src/resource', 'script/dl-i18n.js'],
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user