feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1 @@
src/resource

View 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.

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View 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');
});
});

View 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');
});
});

View File

@@ -0,0 +1,12 @@
{
"operationSettings": [
{
"operationName": "test:cov",
"outputFolderNames": ["coverage"]
},
{
"operationName": "ts-check",
"outputFolderNames": ["./dist"]
}
]
}

View File

@@ -0,0 +1,5 @@
{
"codecov": {
"incrementCoverage": 80
}
}

View File

@@ -0,0 +1,7 @@
const { defineConfig } = require('@coze-arch/eslint-config');
module.exports = defineConfig({
packageRoot: __dirname,
preset: 'web',
rules: {},
});

View 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"
}
}

View 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' />

View 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 };

View 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>
);
}
}

View 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 };

View 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 };

View 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 Detectori18next 底层 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;
}
}

View 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 };

View 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 }

View 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 };

View 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;

View 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"
}
]
}

View File

@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"composite": true
},
"references": [
{
"path": "./tsconfig.build.json"
},
{
"path": "./tsconfig.misc.json"
}
],
"exclude": ["**/*"]
}

View 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
}
}

View 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'],
},
},
});