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