que*_*one 3 i18next reactjs next.js
我需要在nextjs项目中从locize集成 i18 本地化。我发现react-i18next与 i18 和 locize 配合得很好,但不能与 nextjs 集成。另一方面,next-i18next适用于 nextjs 和本地 i18 文件,但似乎不适用于 locize(几乎没有示例)。还有其他解决方案可以使用吗?这可以通过 next-i18next 完成吗?
谢谢。
Edit March 2020: Check https://github.com/UnlyEd/next-right-now boilerplate, which uses the below configuration and provide a real use-case example with Next.js 9 (serverless) + i18next/react-i18next + Locize. (Disclaimer: I'm the author)
Thank you @quebone for your auto-answer. I used it to improve my own configuration, which is using TypeScript with Next.js, but I'm not using next-i18next
like you because it's not compatible yet with the Next serverless
mode.
So, if you're using Next in serverless mode (with Zeit now, for instance), rather follow the following configuration.
utils/i18nextLocize.ts
import { isBrowser } from '@unly/utils';
import { createLogger } from '@unly/utils-simple-logger';
import i18next from 'i18next';
import map from 'lodash.map';
import { initReactI18next } from 'react-i18next';
import { LOCALE_EN, LOCALE_FR } from './locale';
const logger = createLogger({
label: 'utils/i18nextLocize',
});
/**
* Common options shared between all locize/i18next plugins
*
* @see https://github.com/locize/i18next-node-locize-backend#backend-options
* @see https://github.com/locize/i18next-locize-backend#backend-options
* @see https://github.com/locize/locize-node-lastused#options
* @see https://github.com/locize/locize-editor#initialize-with-optional-options
*/
export const locizeOptions = {
projectId: '7867a172-62dc-4f47-b33c-1785c4701b12',
apiKey: isBrowser() ? null : process.env.LOCIZE_API_KEY, // XXX Only define the API key on the server, for all environments (allows to use saveMissing)
version: process.env.APP_STAGE === 'production' ? 'production' : 'latest', // XXX On production, use a dedicated production version
referenceLng: 'fr',
};
/**
* Specific options for the selected Locize backend.
*
* There are different backends for locize, depending on the runtime (browser or node).
* But each backend shares a common API.
*
* @see https://github.com/locize/i18next-node-locize-backend#backend-options
* @see https://github.com/locize/i18next-locize-backend#backend-options
*/
export const locizeBackendOptions = {
...locizeOptions,
loadPath: 'https://api.locize.io/{{projectId}}/{{version}}/{{lng}}/{{ns}}',
addPath: 'https://api.locize.io/missing/{{projectId}}/{{version}}/{{lng}}/{{ns}}',
allowedAddOrUpdateHosts: [
'localhost',
],
};
/**
* Configure i18next with Locize backend.
*
* - Initialized with pre-defined "lang" (to make sure GraphCMS and Locize are configured with the same language)
* - Initialized with pre-fetched "defaultLocales" (for SSR compatibility)
* - Fetches translations from Locize backend
* - Automates the creation of missing translations using "saveMissing: true"
* - Display Locize "in-context" Editor when appending "/?locize=true" to the url (e.g http://localhost:8888/?locize=true)
* - Automatically "touches" translations so it's easier to know when they've been used for the last time,
* helping translators figuring out which translations are not used anymore so they can delete them
*
* XXX We don't rely on https://github.com/i18next/i18next-browser-languageDetector because we have our own way of resolving the language to use, using utils/locale
*
* @param lang
* @param defaultLocales
*/
const i18nextLocize = (lang, defaultLocales): void => {
logger.info(JSON.stringify(defaultLocales, null, 2), 'defaultLocales');
// Plugins will be dynamically added at runtime, depending on the runtime (node or browser)
const plugins = [ // XXX Only plugins that are common to all runtimes should be defined by default
initReactI18next, // passes i18next down to react-i18next
];
// Dynamically load different modules depending on whether we're running node or browser engine
if (!isBrowser()) {
// XXX Use "__non_webpack_require__" on the server
// loads translations, saves new keys to it (saveMissing: true)
// https://github.com/locize/i18next-node-locize-backend
const i18nextNodeLocizeBackend = __non_webpack_require__('i18next-node-locize-backend');
plugins.push(i18nextNodeLocizeBackend);
// sets a timestamp of last access on every translation segment on locize
// -> safely remove the ones not being touched for weeks/months
// https://github.com/locize/locize-node-lastused
const locizeNodeLastUsed = __non_webpack_require__('locize-node-lastused');
plugins.push(locizeNodeLastUsed);
} else {
// XXX Use "require" on the browser, always take the "default" export specifically
// loads translations, saves new keys to it (saveMissing: true)
// https://github.com/locize/i18next-locize-backend
// eslint-disable-next-line @typescript-eslint/no-var-requires
const i18nextLocizeBackend = require('i18next-locize-backend').default;
plugins.push(i18nextLocizeBackend);
// InContext Editor of locize ?locize=true to show it
// https://github.com/locize/locize-editor
// eslint-disable-next-line @typescript-eslint/no-var-requires
const locizeEditor = require('locize-editor').default;
plugins.push(locizeEditor);
}
const i18n = i18next;
map(plugins, (plugin) => i18n.use(plugin));
i18n.init({ // XXX See https://www.i18next.com/overview/configuration-options
resources: defaultLocales,
debug: process.env.APP_STAGE !== 'production',
saveMissing: true,
lng: lang, // XXX We don't use the built-in i18next-browser-languageDetector because we have our own way of detecting language, which must behave identically for both GraphCMS I18n and react-I18n
fallbackLng: lang === LOCALE_FR ? LOCALE_EN : LOCALE_FR,
ns: 'common',
defaultNS: 'common',
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
backend: locizeBackendOptions,
locizeLastUsed: locizeOptions,
editor: {
...locizeOptions,
onEditorSaved: async (lng, ns): Promise<void> => {
// reload that namespace in given language
await i18next.reloadResources(lng, ns);
// trigger an event on i18n which triggers a rerender
// based on bindI18n below in react options
i18next.emit('editorSaved');
},
},
react: {
bindI18n: 'languageChanged editorSaved',
useSuspense: false, // Not compatible with SSR
},
load: 'languageOnly', // Remove if you want to use localization (en-US, en-GB)
});
};
export default i18nextLocize;
Run Code Online (Sandbox Code Playgroud)
Also, because Next will render (SSR) early (it won't wait for i18next to load the initial translations), this will cause the translations not to be fetched by the server and the page served by SSR will not contain the right sentences.
To avoid that, you need to manually pre-fetch all the namespaces you rely on for the current language. This step must be done in pages/_app.tsx
in the getInitialProps
function. (I'm using TSX, but you can use jsx, js, etc.)
pages/_app.tsx
import { ApolloProvider } from '@apollo/react-hooks';
import fetch from 'isomorphic-unfetch';
import get from 'lodash.get';
import { NextPageContext } from 'next';
import NextApp from 'next/app';
import React from 'react';
import Layout from '../components/Layout';
import withData from '../hoc/withData';
import i18nextLocize, { backendOptions } from '../utils/i18nextLocize';
import { LOCALE_FR, resolveBestCountryCodes, resolveBrowserBestCountryCodes } from '../utils/locale';
class App extends NextApp {
/**
* Initialise the application
*
* XXX Executed on the server-side only
*
* @param props
* @see https://github.com/zeit/next.js/#fetching-data-and-component-lifecycle
*/
static async getInitialProps(props): Promise<any> {
const { ctx } = props;
const { req, res }: NextPageContext = ctx;
let publicHeaders = {};
let bestCountryCodes;
if (req) {
bestCountryCodes = resolveBestCountryCodes(req, LOCALE_FR);
const { headers } = req;
publicHeaders = {
'accept-language': get(headers, 'accept-language'),
'user-agent': get(headers, 'user-agent'),
'host': get(headers, 'host'),
};
} else {
bestCountryCodes = resolveBrowserBestCountryCodes();
}
const lang = get(bestCountryCodes, '[0]', 'en').toLowerCase(); // TODO Should return a locale, not a lang. i.e: fr-FR instead of fr
// calls page's `getInitialProps` and fills `appProps.pageProps` - XXX See https://nextjs.org/docs#custom-app
const appProps = await NextApp.getInitialProps(props);
// Pre-fetching locales for i18next, for the "common" namespace
// XXX We do that because if we don't, then the SSR fails at fetching those locales using the i18next "backend" and renders too early
// This hack helps fix the SSR issue
// On the other hand, it seems that once the i18next "resources" are set, they don't change for that language
// so this workaround could cause sync issue if we were using multiple namespaces, but we aren't and probably won't
const defaultLocalesResponse = await fetch(
backendOptions
.loadPath
.replace('{{projectId}}', backendOptions.projectId)
.replace('{{version}}', backendOptions.version)
.replace('{{lng}}', lang)
.replace('{{ns}}', 'common'));
const defaultLocales = {
[lang]: {
common: await defaultLocalesResponse.json(),
}
};
appProps.pageProps = {
...appProps.pageProps,
bestCountryCodes, // i.e: ['EN', 'FR']
lang, // i.e: 'en'
defaultLocales: defaultLocales,
};
return { ...appProps };
}
render() {
const { Component, pageProps, apollo }: any = this.props;
i18nextLocize(pageProps.lang, pageProps.defaultLocales); // Apply i18next configuration with Locize backend
// Workaround for https://github.com/zeit/next.js/issues/8592
const { err }: any = this.props;
const modifiedPageProps = { ...pageProps, err };
return (
<ApolloProvider client={apollo}>
<Layout {...modifiedPageProps}>
<Component {...modifiedPageProps} />
</Layout>
</ApolloProvider>
);
}
componentDidCatch(error, errorInfo) {
// This is needed to render errors correctly in development / production
super.componentDidCatch(error, errorInfo);
}
}
// Wraps all components in the tree with the data provider
export default withData(App);
Run Code Online (Sandbox Code Playgroud)
Then, you can use either HOC or Hook to use translation within your pages/components. Here is an example using HOC with my index page:
pages/index.tsx
import React from 'react';
import { withTranslation } from 'react-i18next';
import { compose } from 'recompose';
import Head from '../components/Head';
const Home = (props: any) => {
const { organisationName, bestCountryCodes, t } = props;
return (
<div>
<Head />
<div className="hero">
<div>{t('welcome', 'Bonjour auto')}</div>
<div>{t('missingShouldBeAdded', 'Missing sentence, should be added automatically')}</div>
<div>{t('missingShouldBeAdded2', 'Missing sentence, should be added automatically')}</div>
</div>
</div>
);
};
export default compose(
withTranslation(['common']),
)(Home);
Run Code Online (Sandbox Code Playgroud)
See official documentation for more examples:
Note that auto adding of missing sentence doesn't work for me on localhost, but works fine online.
Edit: Made it work in localhost eventually, not sure how.
Note that you'll need to install additional dependencies to have this working:
@types/webpack-env
: Allows to use__non_webpack_require__
with TypeScript.
Also note that I use a custom locale detector, but you would probably want to use the recommended one at https://github.com/i18next/i18next-browser-languageDetector
归档时间: |
|
查看次数: |
5505 次 |
最近记录: |