From b7b65bcaf0d4e82c5ac129a05ce7054b07940143 Mon Sep 17 00:00:00 2001 From: Tom Esterez Date: Wed, 1 Dec 2021 21:54:07 +0100 Subject: [PATCH] Add support for nsSeparator and defaultNS (#739) * Support custom nsSeparator and defaultNS * Trans supports ns prop * remove console.log * README * refactor + doc --- README.md | 4 +++ __tests__/Trans.test.js | 17 ++++++++++++ __tests__/useTranslation.test.js | 45 ++++++++++++++++++++++++++++++++ src/Trans.tsx | 3 ++- src/index.tsx | 6 ++++- src/transCore.tsx | 24 ++++++++++++++--- src/useTranslation.tsx | 4 +-- src/withTranslation.tsx | 4 +-- src/wrapTWithDefaultNs.tsx | 23 +++------------- 9 files changed, 101 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 93369a77..9d4bc799 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,8 @@ In the configuration file you can use both the configuration that we specified h | `loader` | If you wish to disable the webpack loader and manually load the namespaces on each page, we give you the opportunity to do so by disabling this option. | `Boolean` | `true` | | `interpolation` | Change the delimeter that is used for interpolation. | `{prefix: string; suffix: string, formatter: function }` | `{prefix: '{{', suffix: '}}'}` | `keySeparator` | Change the separator that is used for nested keys. Set to `false` to disable keys nesting in JSON translation files. Can be useful if you want to use natural text as keys. | `string` | `false` | `'.'` +| `nsSeparator` | char to split namespace from key. You should set it to `false` if you want to use natural text as keys. | `string` | `false` | `':'` +| `defaultNS` | default namespace used if not passed to `useTranslation` or in the translation key. | `string` | `undefined` | `staticsHoc` | The HOCs we have in our API ([appWithI18n](#appwithi18n)), do not use [hoist-non-react-statics](https://github.com/mridgway/hoist-non-react-statics) in order not to include more kb than necessary _(static values different than getInitialProps in the pages are rarely used)_. If you have any conflict with statics, you can add hoist-non-react-statics (or any other alternative) here. [See an example](docs/hoist-non-react-statics.md). | `Function` | `null` | `extensionsRgx` | Change the regex used by the webpack loader to find Next.js pages. | `Regex` | `/\.(tsx\|ts\|js\|mjs\|jsx)$/` | `pagesInDir` | If you run `next ./my-app` to change where your pages are, you can here define `my-app/pages` so that next-translate can guess where they are. | `String` | If you don't define it, by default the pages will be searched for in the classic places like `pages` and `src/pages`. @@ -281,6 +283,7 @@ The `t` function: - **fallback**: string | string[] - fallback if i18nKey doesn't exist. [See more](#8-fallbacks). - **returnObjects**: boolean - Get part of the JSON with all the translations. [See more](#7-nested-translations). - **default**: string - Default translation for the key. If fallback keys are used, it will be used only after exhausting all the fallbacks. + - **ns**: string - Namespace to use when none is embded in the `i18nKey`. - **Output**: string ### withTranslation @@ -355,6 +358,7 @@ Or using `components` prop as a object: - `values` - Object - query params - `fallback` - string | string[] - Optional. Fallback i18nKey if the i18nKey doesn't match. - `defaultTrans` - string - Default translation for the key. If fallback keys are used, it will be used only after exhausting all the fallbacks. + - `ns` - Namespace to use when none is embedded in `i18nKey` In cases where we require the functionality of the `Trans` component, but need a **string** to be interpolated, rather than the output of the `t(props.i18nKey)` function, there is also a `TransText` component, which takes a `text` prop instead of `i18nKey`. diff --git a/__tests__/Trans.test.js b/__tests__/Trans.test.js index f694d2d9..fe2a6a8f 100644 --- a/__tests__/Trans.test.js +++ b/__tests__/Trans.test.js @@ -93,6 +93,23 @@ describe('Trans', () => { expect(container.textContent).toContain(expected) }) + test('should work with ns prop', () => { + const i18nKey = 'number' + const expected = 'The number is 42' + const ns = { + number: 'The number is 42', + } + + const config = { keySeparator: false } + + const { container } = render( + + + + ) + expect(container.textContent).toContain(expected) + }) + test('should work the same way than useTranslate with default value', () => { const i18nKey = 'ns:number' const expected = 'The number is 42' diff --git a/__tests__/useTranslation.test.js b/__tests__/useTranslation.test.js index de3bacba..ebb5f3df 100644 --- a/__tests__/useTranslation.test.js +++ b/__tests__/useTranslation.test.js @@ -160,6 +160,51 @@ describe('useTranslation', () => { expect(container.textContent).toBe(expected) }) + test('should work with custom nsSeparator', () => { + const Inner = () => { + const { t } = useTranslation('a') + return t`b||test` + } + + const ns = { + a: { test: 'Test from A' }, + b: { test: 'Test from B' }, + } + + const expected = 'Test from B' + + const { container } = render( + + + + ) + expect(container.textContent).toBe(expected) + }) + + test('should work with natural text as key and defaultNS', () => { + const Inner = () => { + const { t } = useTranslation() + return t`progress: loading...` + } + + const ns = { + a: { 'progress: loading...': 'progression: chargement...' }, + } + + const expected = 'progression: chargement...' + + const { container } = render( + + + + ) + expect(container.textContent).toBe(expected) + }) + test('should work with a defined default namespace | t as function', () => { const Inner = () => { const { t } = useTranslation('a') diff --git a/src/Trans.tsx b/src/Trans.tsx index 6beb2861..893c763f 100644 --- a/src/Trans.tsx +++ b/src/Trans.tsx @@ -15,8 +15,9 @@ export default function Trans({ components, fallback, defaultTrans, + ns, }: TransProps): any { - const { t, lang } = useTranslation() + const { t, lang } = useTranslation(ns) /** * Memoize the transformation diff --git a/src/index.tsx b/src/index.tsx index 20ecde82..413138d4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,6 +13,7 @@ export type Translate = ( returnObjects?: boolean fallback?: string | string[] default?: string + ns?: string } ) => T @@ -34,6 +35,7 @@ export interface TransProps { values?: TranslationQuery fallback?: string | string[] defaultTrans?: string + ns?: string } export type PageValue = string[] | ((context: object) => string[]) @@ -58,6 +60,8 @@ export interface I18nConfig { suffix: string } keySeparator?: string | false + nsSeparator?: string | false + defaultNS?: string } export interface LoaderConfig extends I18nConfig { @@ -71,7 +75,7 @@ export interface LoaderConfig extends I18nConfig { } export interface LoggerProps { - namespace: string + namespace: string | undefined i18nKey: string } diff --git a/src/transCore.tsx b/src/transCore.tsx index 8736b6f8..426ccfa7 100644 --- a/src/transCore.tsx +++ b/src/transCore.tsx @@ -7,6 +7,16 @@ import { } from '.' import { Translate } from './index' +function splitNsKey(key: string, nsSeparator: string | false) { + if (!nsSeparator) return { i18nKey: key } + const i = key.indexOf(nsSeparator) + if (i < 0) return { i18nKey: key } + return { + namespace: key.slice(0, i), + i18nKey: key.slice(i + nsSeparator.length), + } +} + export default function transCore({ config, allNamespaces, @@ -22,8 +32,14 @@ export default function transCore({ const t: Translate = (key = '', query, options) => { const k = Array.isArray(key) ? key[0] : key - const [namespace, i18nKey] = k.split(/:(.+)/) - const dic = allNamespaces[namespace] || {} + const { nsSeparator = ':' } = config + + const { i18nKey, namespace = options?.ns ?? config.defaultNS } = splitNsKey( + k, + nsSeparator + ) + + const dic = (namespace && allNamespaces[namespace]) || {} const keyWithPlural = plural(pluralRules, dic, i18nKey, config, query) const value = getDicValue(dic, keyWithPlural, config, options) @@ -221,9 +237,9 @@ function missingKeyLogger({ namespace, i18nKey }: LoggerProps): void { if (process.env.NODE_ENV === 'production') return // This means that instead of "ns:value", "value" has been misspelled (without namespace) - if (!i18nKey) { + if (!namespace) { console.warn( - `[next-translate] The text "${namespace}" has no namespace in front of it.` + `[next-translate] The text "${i18nKey}" has no namespace in front of it.` ) return } diff --git a/src/useTranslation.tsx b/src/useTranslation.tsx index 92e56b1f..aa389b75 100644 --- a/src/useTranslation.tsx +++ b/src/useTranslation.tsx @@ -3,10 +3,10 @@ import { I18n } from '.' import wrapTWithDefaultNs from './wrapTWithDefaultNs' import I18nContext from './_context' -export default function useTranslation(defaultNs?: string): I18n { +export default function useTranslation(defaultNS?: string): I18n { const ctx = useContext(I18nContext) return { ...ctx, - t: wrapTWithDefaultNs(ctx.t, defaultNs), + t: wrapTWithDefaultNs(ctx.t, defaultNS), } } diff --git a/src/withTranslation.tsx b/src/withTranslation.tsx index 36b35697..5a7d0fb4 100644 --- a/src/withTranslation.tsx +++ b/src/withTranslation.tsx @@ -7,10 +7,10 @@ import { NextComponentType } from 'next' */ export default function withTranslation

( Component: React.ComponentType

| NextComponentType, - defaultNs?: string + defaultNS?: string ): React.ComponentType> { const WithTranslation: NextComponentType = (props: P) => { - const i18n = useTranslation(defaultNs) + const i18n = useTranslation(defaultNS) return } diff --git a/src/wrapTWithDefaultNs.tsx b/src/wrapTWithDefaultNs.tsx index 9136be84..cb5d11c6 100644 --- a/src/wrapTWithDefaultNs.tsx +++ b/src/wrapTWithDefaultNs.tsx @@ -2,28 +2,13 @@ import { Translate } from '.' export default function wrapTWithDefaultNs( oldT: Translate, - defaultNs?: string + ns?: string ): Translate { - if (typeof defaultNs !== 'string') return oldT + if (typeof ns !== 'string') return oldT // Use default namespace if namespace is missing - const t: Translate = (key = '', query, options) => { - let k = Array.isArray(key) ? key[0] : key - if (!k.includes(':')) k = `${defaultNs}:${k}` - - // Use default namespace for query.fallback keys - if (options?.fallback) { - if (Array.isArray(options.fallback)) { - options.fallback = options.fallback.map((k) => - k.includes(':') ? k : `${defaultNs}:${k}` - ) - } else { - const k = options.fallback - options.fallback = k.includes(':') ? k : `${defaultNs}:${k}` - } - } - - return oldT(k, query, options) + const t: Translate = (key, query, options) => { + return oldT(key, query, { ns, ...options }) } return t