Some problems and solutions encountered in the initial experience of internationalization of spring cloud & react project

background

Recently, possible foreign customers have appeared in the project in charge, so the system should be internationalized in order to develop the international market. After all, making dollars is fun. The front end of the project is separated from the front end. The front end is developed based on React, and the back end is spring cloud. The background is probably like this. The goal is always to internationalize the system in order to support international users.

Contents of international transformation

I looked through the articles on Internationalization on the Internet. Most of them are concentrated in the front desk or background, and there is no more systematic thing. Here, let's sort out the contents involved in complete internationalization.

  • The internationalization of front-end text resources is very obvious, that is, the internationalization of text content
  • The internationalization of other front-end resources is easy to ignore, especially when it involves some pictures or music, which need special treatment
  • The internationalization of the back-end returned content, such as some error prompts, does not involve storage
  • The internationalization of back-end saved content, such as some logs, some special logs may be displayed to users, but they need to be saved
  • The backend actively pushes the internationalization of content. These are special, such as real-time reminders

The more mature solutions are the internationalization of front-end text resources and the internationalization of back-end returned content. Other have not found a more mature solution. I will only introduce the general idea here.

International transformation

International transformation of front desk

At the front end, we adopt react framework, and internationalization naturally adopts react-i18next.

package.json add dependencies. "react-i18next": "11.12.0",

Add I18N JS in app JS code in the main directory.

//i18n.js
import i18n from 'i18next';
import {initReactI18next} from 'react-i18next';
import {getStorageItem} from "../utils/localStorage";
import {taskCreateCn, taskCreateEn} from './taskCreateCn';

const resources = {
    zh_CN: {
        translation: {
            'locale': 'zh_CN',
            'login': 'Sign in',
            'chrome': 'Google browser',
           
        }
    },
    en_US: {
        translation: {
            'locale': 'enUS',
            'login': 'login',
            'chrome': 'chrome',
            
        }
    },
};
const selectedLanguage = getStorageItem('language');
i18n
    .use(initReactI18next) // passes i18n down to react-i18next
    .init({
        resources,
        lng: selectedLanguage ? selectedLanguage : 'zh_CN',
        keySeparator: false, // we do not use keys in form messages.welcome
        interpolation: {
            escapeValue: false, // react already safes from xss
        },
    }).then();

export default i18n;

app.js, focusing on the introduction of I18N js

import "@babel/polyfill";
import {ConfigProvider} from 'antd';
import 'antd/dist/antd.css';
import enUS from 'antd/lib/locale/en_US';
import zh_CN from 'antd/lib/locale-provider/zh_CN';
import 'core-js/es6/map';
import 'core-js/es6/set';
import 'raf/polyfill';
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
// import '../utils/mock';
import store from './redux/Store';
import Router from './Router';
import './i18n'; // Import I18N here js
import {useTranslation} from "react-i18next";

const App = () => {
    const {t, i18n} = useTranslation()
    React.translate = t

    return (
        t('locale') === 'zh_CN' ?
            <ConfigProvider locale={zh_CN}>
                <Provider store={store}>
                    <Router/>
                </Provider>
            </ConfigProvider>
            :
            <ConfigProvider locale={enUS}>
                <Provider store={store}>
                    <Router/>
                </Provider>
            </ConfigProvider>
    )
}

ReactDOM.render(<App/>, document.getElementById('content'))

Specific page code

import React, {useEffect} from 'react';
import {Button, Col, Divider, Form, Input, message, Row, Select, Tooltip} from 'antd';
import {getStorageItem, setStorageItem} from "../../utils/localStorage"
import {useTranslation} from 'react-i18next'

const Login = () => {
    const {t, i18n} = useTranslation();
    const selectedLanguage = getStorageItem('language');
    return (
        <div className='login-background'>
            <Row className='top' justify={"center"}>
                <Col>
                    <div className='title'><span>{t('chrome')}</span></div>
                </Col>

            </Row>

        </div>
    )
}

export default Login;

The foreground needs to cooperate with the interceptor to increase the internationalization header for background requests

// Add response interceptor
axios.interceptors.request.use(function (config){
    config.headers.Local="zh_CN";
    return config;
})

The basic meaning has been in the sample code. const {t, i18n} = useTranslation(); is introduced into the component;, The original text is replaced by t('key '), and then the text is in I18N JS gives the corresponding text in various languages.

International transformation of the back end

The internationalization transformation of the back-end is more complex because the system is distributed, so the language information needs to be transmitted to various services through the call chain, but the traditional internationalization method can not cross services, especially when various services call each other through Feign.

Basic idea: what we use is to add a custom LoaclResovlery and a custom Feign interceptor to realize the transfer of international headers. Internationalization is achieved through standard springboot internationalization.

Inject LocaleResolver into each service. Note the Locale used in the internationalization header here.


/**
 * @author zhaoe
 */
@Component
public class LocaleConfig {

    public static final String LOCAL_HEAD_NAME = "Locale";

    @Bean
    public LocaleResolver localeResolver() {
        LocaleHeaderLocaleResolver localeResolver = new LocaleHeaderLocaleResolver();
        localeResolver.setLocaleHeadName(LOCAL_HEAD_NAME);
        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        return localeResolver;
    }

    @Bean
    public WebMvcConfigurer localeInterceptor() {
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(@Nonnull InterceptorRegistry registry) {
                LocaleChangeInterceptor localeInterceptor = new LocaleChangeInterceptor();
                localeInterceptor.setParamName(LOCAL_HEAD_NAME);
                registry.addInterceptor(localeInterceptor);
            }
        };
    }
}

Custom LocaleContextResolver


/**
 * @author zew
 */
public class LocaleHeaderLocaleResolver implements LocaleContextResolver {

    public static final String LOCALE_REQUEST_ATTRIBUTE_NAME = LocaleHeaderLocaleResolver.class.getName() + ".LOCALE";

    public static final String TIME_ZONE_REQUEST_ATTRIBUTE_NAME = LocaleHeaderLocaleResolver.class.getName() + ".TIME_ZONE";

    @Nullable
    private Locale defaultLocale;
    @Nullable
    private TimeZone defaultTimeZone;

    @Nullable
    private String localeHeadName;

    public void setLocaleHeadName(@Nullable String localeHeadName) {
        this.localeHeadName = localeHeadName;
    }

    @Nullable
    public String getLocaleHeadName() {
        return this.localeHeadName;
    }

    public void setDefaultLocale(@Nullable Locale defaultLocale) {
        this.defaultLocale = defaultLocale;
    }

    @Nullable
    public Locale getDefaultLocale() {
        return this.defaultLocale;
    }

    public void setDefaultTimeZone(@Nullable TimeZone defaultTimeZone) {
        this.defaultTimeZone = defaultTimeZone;
    }

    @Nullable
    protected TimeZone getDefaultTimeZone() {
        return this.defaultTimeZone;
    }

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        parseLocaleHeaderIfNecessary(request);
        return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
    }

    @Override
    public LocaleContext resolveLocaleContext(final HttpServletRequest request) {
        parseLocaleHeaderIfNecessary(request);
        return new TimeZoneAwareLocaleContext() {
            @Override
            @Nullable
            public Locale getLocale() {
                return (Locale) request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME);
            }

            @Override
            @Nullable
            public TimeZone getTimeZone() {
                return (TimeZone) request.getAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME);
            }
        };
    }

    @Override
    public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response,
                                 @Nullable LocaleContext localeContext) {
        Assert.notNull(response, "HttpServletResponse is required for LocaleHeaderLocaleResolver");

        Locale locale = null;
        TimeZone timeZone = null;
        if (localeContext != null) {
            locale = localeContext.getLocale();
            if (localeContext instanceof TimeZoneAwareLocaleContext) {
                timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone();
            }
            response.setHeader(getLocaleHeadName(),
                    (locale != null ? locale.toString() : "-") + (timeZone != null ? ' ' + timeZone.getID() : ""));
        }
        request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
                (locale != null ? locale : determineDefaultLocale(request)));
        request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
                (timeZone != null ? timeZone : determineDefaultTimeZone(request)));
    }

    private void parseLocaleHeaderIfNecessary(HttpServletRequest request) {
        if (request.getAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME) != null) {
            return;
        }

        String headName = getLocaleHeadName();
        Locale locale = processLocale(request, headName);
        request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
                (locale != null ? locale : determineDefaultLocale(request)));
        TimeZone timeZone = processTimeZone(request, headName);

        request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
                (timeZone != null ? timeZone : determineDefaultTimeZone(request)));

    }

    private Locale processLocale(HttpServletRequest request, String headName) {
        Locale locale = null;
        if (headName == null) {
            return null;
        }
        String localeStr = request.getHeader(headName);
        if (localeStr != null) {
            String localePart = localeStr;
            int spaceIndex = localePart.indexOf(' ');
            if (spaceIndex != -1) {
                localePart = localeStr.substring(0, spaceIndex);
            }
            try {
                locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null);
            } catch (IllegalArgumentException ex) {
                if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
                    throw new IllegalStateException("Invalid locale Header '" + getLocaleHeadName() +
                            "' with value [" + localeStr + "]: " + ex.getMessage());
                }
            }
        }
        return locale;
    }

    private TimeZone processTimeZone(HttpServletRequest request, String headName) {
        TimeZone timeZone = null;
        if (headName == null) {
            return null;
        }
        String localeStr = request.getHeader(headName);
        if (localeStr != null) {
            String timeZonePart = null;
            int spaceIndex = localeStr.indexOf(' ');
            if (spaceIndex != -1) {
                timeZonePart = localeStr.substring(spaceIndex + 1);
            }
            try {
                if (timeZonePart != null) {
                    timeZone = StringUtils.parseTimeZoneString(timeZonePart);
                }
            } catch (IllegalArgumentException ex) {
                if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
                    throw new IllegalStateException("Invalid locale Header '" + getLocaleHeadName() +
                            "' with value [" + localeStr + "]: " + ex.getMessage());
                }
            }
        }
        return timeZone;
    }

    @Nullable
    protected Locale determineDefaultLocale(HttpServletRequest request) {
        if (getDefaultLocale() == null) {
            return request.getLocale();
        } else {
            return getDefaultLocale();
        }
    }

    @Nullable
    protected TimeZone determineDefaultTimeZone(HttpServletRequest request) {
        return getDefaultTimeZone();
    }

    @Nullable
    protected Locale parseLocaleValue(String locale) {
        return StringUtils.parseLocaleString(locale);
    }

    @Override
    public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) {
        setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null));
    }

}

Special text dynamic nationalization tools


/**
 * International chemical tools
 *
 * @author zhaoe
 */
@Component
public class MessageI18nUtils {
    private static MessageSource messageSource;

    public MessageI18nUtils(MessageSource messageSource) {
        MessageI18nUtils.messageSource = messageSource;
    }

    /**
     * Get a single international translation value
     */
    public static String get(String msgKey) {
        try {
            return messageSource.getMessage(msgKey, null, LocaleContextHolder.getLocale());
        } catch (Exception e) {
            return msgKey;
        }
    }
}

Then create I18N / messages in the resource folder properties,i18n/messages_ zh_ CN. properties,i18n/messages_ en_ US. Properties file. Each property describes the text corresponding to the key in KV.
By the way, don't forget application Add the following configuration information to YML.

spring:
  messages:
    encoding: UTF-8
    basename: i18n/messages

ok, the above is the traditional internationalization, and the following deals with the special header transmission problem of distributed services. The solution here only applies to Feign.
The RequestInterceptor here is Feign's interceptor. Manually add an upper header through the interceptor to transfer the international header to the downstream service.


/**
 * @author zew
 */
@Slf4j
@Configuration
public class FeignConfig implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (Objects.isNull(attributes)) {
            return;
        }
        HttpServletRequest request = attributes.getRequest();
        Enumeration<String> headerNames = request.getHeaderNames();
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                if (StringUtils.equalsIgnoreCase(name, LocaleConfig.LOCAL_HEAD_NAME)) {
                    String values = request.getHeader(name);
                    requestTemplate.header(name, values);
                }

            }
        }
    }
}

In this way, messagei18nutils is used when recording Get to get the dynamic text content of the request.

Other transformation ideas

Internationalization of saved content

There are many ways to realize the internationalization of saving content.
1. Double writing can be adopted, that is, the contents in multiple languages can be saved, and the corresponding contents can be returned according to Local during front-end query.
2. The saved content can also be in the form of key. When returning to the foreground, the Local requested by the foreground will be converted to the corresponding content.
3. Of course, there are wild ways. You can directly store only one language, and then return to the foreground. If other languages are detected, call the translation API for real-time content conversion and return to the foreground.

The third route has the advantages of fast implementation and less storage, but the way is a little wild and the translation quality is uneven.

Internationalization of push content

We push by message subscription.
For the internationalization of push content, we use the method of separate push, that is, messagei18nutils Get (key, local) gives content in different languages and pushes it to different topic s.
The front end adjusts and subscribes to different reminder topics in the switching language, and subscribes to us in English_ Us / original topic, Chinese subscription cn_ZH / original topic. In this way, the content will be displayed directly.

summary

Internationalization is actually a very systematic process, which can not be explained in an article here. Therefore, there may be many problems that need to be carefully considered and adjusted according to their own business conditions.

Keywords: React Spring Cloud Microservices

Added by mhenke on Sun, 16 Jan 2022 04:33:20 +0200