Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,4 @@ build
pnpm-lock.yaml
package-lock.json
example
.DS_Store
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ const BaseExample = () => {
/>
<FormattedMessage id="hello" />
<div>----------------</div>
<ChildrenComponent locale={locale} />
<ChildrenComponent />
</Flex>
</IntlProvider>
);
Expand Down
2 changes: 1 addition & 1 deletion doc/multi-level.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const BaseExample = () => {
/>
<FormattedMessage id="hello" />
<div>----------------</div>
<ChildrenComponent locale={locale} />
<ChildrenComponent />
</Flex>
</IntlProvider>
);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kne/react-intl",
"version": "0.1.12",
"version": "0.1.13",
"description": "快捷地创建国际化中需要使用到的组件",
"syntax": {
"esmodules": true
Expand Down
84 changes: 53 additions & 31 deletions src/createWithFetchLang.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,78 @@
import Fetch from '@kne/react-fetch';
import { useGlobalValue, usePreset } from '@kne/global-context';
import localeLoader, { messagesLoader, message } from './loader';
import { IntlProvider, useIntl } from 'react-intl';
import React, { forwardRef } from 'react';
import { IntlProvider, IntlContext } from 'react-intl';
import React, { forwardRef, useContext } from 'react';
import { Provider as MessageProvider, useContext as useMessageContext } from './contex';
import { argsParse, resolveLocale, mergeIntlMessages, hasMessages } from './intlUtils';

const argsParse = (...args) => {
if (typeof args[0] === 'object' && typeof args[0].defaultLocale === 'string') {
return Object.assign({}, args[0]);
}

return { defaultLocale: args[0], defaultMessage: args[1], namespace: args[2] };
const renderIntlTree = ({ locale, prevMessage, namespaceMessages, WrappedComponents, props, ref, MessageProvider: MsgProvider }) => {
const currentMessage = mergeIntlMessages(prevMessage, namespaceMessages);
return (
<IntlProvider messages={currentMessage} locale={locale}>
<MsgProvider value={currentMessage}>
<WrappedComponents {...props} ref={ref} />
</MsgProvider>
</IntlProvider>
);
};

const createWithFetchLang = (...args) => {
const { defaultLocale, defaultMessage, namespace, messages } = argsParse(...args);
const { defaultLocale, defaultMessage, namespace, messages: configMessages } = argsParse(...args);
defaultMessage && localeLoader(defaultLocale, defaultMessage, namespace);
messages && messagesLoader(messages, namespace);
configMessages && messagesLoader(configMessages, namespace);

return WrappedComponents =>
forwardRef(({ locale: propsLocale, ...props }, ref) => {
const { apis } = usePreset();
const contextLocal = useGlobalValue('locale');
const locale = propsLocale || contextLocal || defaultLocale || 'zh-CN';
const parentIntl = useContext(IntlContext);
const prevMessage = useMessageContext();
const locale = resolveLocale({
propsLocale,
contextLocal,
parentIntlLocale: parentIntl?.locale,
defaultLocale
});
const currentNamespace = namespace || 'global';
const messages = message[locale]?.[currentNamespace];
const namespaceMessages = message[locale]?.[currentNamespace];
const defaultLocalMessage = message[defaultLocale || 'zh-CN']?.[currentNamespace];
if (apis?.localeMessage && !(messages && Object.keys(messages).length > 0) && defaultLocalMessage && Object.keys(defaultLocalMessage).length > 0) {
const needsRemoteFetch = apis?.localeMessage && !hasMessages(namespaceMessages) && hasMessages(defaultLocalMessage);

if (needsRemoteFetch) {
return (
<Fetch
{...Object.assign({}, apis.localeMessage, { data: { locale, namespace: currentNamespace, defaultLang: defaultLocalMessage } })}
cache="intl-fetch-lang "
{...Object.assign({}, apis.localeMessage, {
data: { locale, namespace: currentNamespace, defaultLang: defaultLocalMessage }
})}
cache="intl-fetch-lang"
render={({ data }) => {
messagesLoader({ [locale]: data }, currentNamespace);
const messages = message[locale]?.[currentNamespace];
return (
<IntlProvider messages={messages} locale={locale}>
<WrappedComponents {...props} ref={ref} />
</IntlProvider>
);
const loadedMessages = hasMessages(data) ? data : defaultLocalMessage;
messagesLoader({ [locale]: loadedMessages }, currentNamespace);
const resolvedMessages = message[locale]?.[currentNamespace] || defaultLocalMessage;
return renderIntlTree({
locale,
prevMessage,
namespaceMessages: resolvedMessages,
WrappedComponents,
props,
ref,
MessageProvider
});
}}
/>
);
}
const prevMessage = useMessageContext();
const currentMessage = Object.assign({}, prevMessage, messages);
return (
<IntlProvider messages={currentMessage} locale={locale}>
<MessageProvider value={currentMessage}>
<WrappedComponents {...props} ref={ref} />
</MessageProvider>
</IntlProvider>
);

return renderIntlTree({
locale,
prevMessage,
namespaceMessages,
WrappedComponents,
props,
ref,
MessageProvider
});
});
};

Expand Down
32 changes: 7 additions & 25 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,16 @@
import React, { forwardRef } from 'react';
import localeLoader, { message, messagesLoader } from './loader';
import React from 'react';
import localeLoader, { message } from './loader';
import { IntlProvider, createIntl as createIntlBase, useIntl } from 'react-intl';
import { useGlobalValue } from '@kne/global-context';
import createWithFetchLang from './createWithFetchLang';

const withIntlProvider = WrappedComponents =>
forwardRef(({ locale: propsLocale, namespace, ...props }, ref) => {
const contextLocal = useGlobalValue('locale');
const locale = propsLocale || contextLocal || 'zh-CN';
return (
<IntlProvider messages={message[locale]?.[namespace || 'global']} locale={locale}>
<WrappedComponents {...props} ref={ref} />
</IntlProvider>
);
});

const argsParse = (...args) => {
if (typeof args[0] === 'object' && typeof args[0].defaultLocale === 'string') {
return Object.assign({}, args[0]);
}

return { defaultLocale: args[0], defaultMessage: args[1], namespace: args[2] };
};
import { hasMessages } from './intlUtils';

export const createIntlProvider = (...args) => {
const withIntlProvider = createWithFetchLang(...args);
const IntlProviderComponent = createWithFetchLang(...args);
const InnerComponent = ({ children }) => {
const intl = useIntl();
return children(intl);
};
return withIntlProvider(({ locale: propsLocale, children }) => {
return IntlProviderComponent(({ locale: propsLocale, children }) => {
return typeof children === 'function' ? <InnerComponent>{children}</InnerComponent> : children;
});
};
Expand All @@ -40,7 +21,8 @@ export * from 'react-intl';

export const createIntl = ({ locale = 'zh-CN', message: propsMessage, namespace }) => {
propsMessage && localeLoader(locale, propsMessage, namespace);
return createIntlBase({ locale, messages: message[locale]?.[namespace || 'global'] });
const namespaceMessages = message[locale]?.[namespace || 'global'];
return createIntlBase({ locale, messages: hasMessages(namespaceMessages) ? namespaceMessages : {} });
};

export { localeLoader, IntlProvider };
Expand Down
22 changes: 22 additions & 0 deletions src/intlUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const argsParse = (...args) => {
if (typeof args[0] === 'object' && args[0] !== null && !Array.isArray(args[0])) {
const config = { ...args[0] };
if (typeof config.defaultLocale !== 'string') {
const messageLocales = config.messages && typeof config.messages === 'object' ? Object.keys(config.messages) : [];
config.defaultLocale = messageLocales[0] || 'zh-CN';
}
return config;
}

const defaultLocale = typeof args[0] === 'string' ? args[0] : 'zh-CN';
return { defaultLocale, defaultMessage: args[1], namespace: args[2] };
};

export const resolveLocale = ({ propsLocale, contextLocal, parentIntlLocale, defaultLocale }) => {
const fallbackDefault = typeof defaultLocale === 'string' ? defaultLocale : 'zh-CN';
return propsLocale || contextLocal || parentIntlLocale || fallbackDefault || 'zh-CN';
};

export const mergeIntlMessages = (prevMessage, namespaceMessages) => Object.assign({}, prevMessage, namespaceMessages);

export const hasMessages = value => Boolean(value && typeof value === 'object' && Object.keys(value).length > 0);
91 changes: 91 additions & 0 deletions src/intlUtils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { argsParse, resolveLocale, mergeIntlMessages, hasMessages } from './intlUtils';

describe('intlUtils', () => {
describe('argsParse', () => {
it('parses object config with defaultLocale', () => {
expect(
argsParse({
defaultLocale: 'en-US',
messages: { 'en-US': { hello: 'Hello' } }
})
).toEqual({
defaultLocale: 'en-US',
messages: { 'en-US': { hello: 'Hello' } }
});
});

it('infers defaultLocale from messages when missing', () => {
expect(
argsParse({
messages: {
'zh-CN': { hello: '你好' },
'en-US': { hello: 'Hello' }
}
})
).toMatchObject({ defaultLocale: 'zh-CN' });
});

it('parses legacy positional args', () => {
expect(argsParse('zh-CN', { hello: '你好' }, 'demo')).toEqual({
defaultLocale: 'zh-CN',
defaultMessage: { hello: '你好' },
namespace: 'demo'
});
});
});

describe('resolveLocale', () => {
it('prefers props locale over parent and default', () => {
expect(
resolveLocale({
propsLocale: 'en-US',
contextLocal: 'ja-JP',
parentIntlLocale: 'de-DE',
defaultLocale: 'zh-CN'
})
).toBe('en-US');
});

it('inherits parent intl locale when props locale is absent', () => {
expect(
resolveLocale({
propsLocale: undefined,
contextLocal: undefined,
parentIntlLocale: 'en-US',
defaultLocale: 'zh-CN'
})
).toBe('en-US');
});

it('falls back when defaultLocale is invalid', () => {
expect(
resolveLocale({
propsLocale: undefined,
contextLocal: undefined,
parentIntlLocale: undefined,
defaultLocale: { messages: {} }
})
).toBe('zh-CN');
});
});

describe('mergeIntlMessages', () => {
it('merges parent and namespace messages', () => {
expect(mergeIntlMessages({ hello: 'parent' }, { title: 'child' })).toEqual({
hello: 'parent',
title: 'child'
});
});
});

describe('hasMessages', () => {
it('returns false for empty values', () => {
expect(hasMessages(undefined)).toBe(false);
expect(hasMessages({})).toBe(false);
});

it('returns true when keys exist', () => {
expect(hasMessages({ hello: 'world' })).toBe(true);
});
});
});
Loading