Skip to content

Commit

Permalink
Add support for nsSeparator and defaultNS (#739)
Browse files Browse the repository at this point in the history
* Support custom nsSeparator and defaultNS

* Trans supports ns prop

* remove console.log

* README

* refactor + doc
  • Loading branch information
testerez authored Dec 1, 2021
1 parent 4d95074 commit b7b65bc
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 29 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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`.

Expand Down
17 changes: 17 additions & 0 deletions __tests__/Trans.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<I18nProvider lang="en" namespaces={{ ns }} config={config}>
<Trans i18nKey={i18nKey} ns="ns" />
</I18nProvider>
)
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'
Expand Down
45 changes: 45 additions & 0 deletions __tests__/useTranslation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<I18nProvider lang="en" namespaces={ns} config={{ nsSeparator: '||' }}>
<Inner />
</I18nProvider>
)
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(
<I18nProvider
lang="en"
namespaces={ns}
config={{ nsSeparator: false, keySeparator: false, defaultNS: 'a' }}
>
<Inner />
</I18nProvider>
)
expect(container.textContent).toBe(expected)
})

test('should work with a defined default namespace | t as function', () => {
const Inner = () => {
const { t } = useTranslation('a')
Expand Down
3 changes: 2 additions & 1 deletion src/Trans.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type Translate = <T = string>(
returnObjects?: boolean
fallback?: string | string[]
default?: string
ns?: string
}
) => T

Expand All @@ -34,6 +35,7 @@ export interface TransProps {
values?: TranslationQuery
fallback?: string | string[]
defaultTrans?: string
ns?: string
}

export type PageValue = string[] | ((context: object) => string[])
Expand All @@ -58,6 +60,8 @@ export interface I18nConfig {
suffix: string
}
keySeparator?: string | false
nsSeparator?: string | false
defaultNS?: string
}

export interface LoaderConfig extends I18nConfig {
Expand All @@ -71,7 +75,7 @@ export interface LoaderConfig extends I18nConfig {
}

export interface LoggerProps {
namespace: string
namespace: string | undefined
i18nKey: string
}

Expand Down
24 changes: 20 additions & 4 deletions src/transCore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)

Expand Down Expand Up @@ -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
}
Expand Down
4 changes: 2 additions & 2 deletions src/useTranslation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
4 changes: 2 additions & 2 deletions src/withTranslation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { NextComponentType } from 'next'
*/
export default function withTranslation<P = unknown>(
Component: React.ComponentType<P> | NextComponentType<any, any, any>,
defaultNs?: string
defaultNS?: string
): React.ComponentType<Omit<P, 'i18n'>> {
const WithTranslation: NextComponentType<any, any, any> = (props: P) => {
const i18n = useTranslation(defaultNs)
const i18n = useTranslation(defaultNS)
return <Component {...props} i18n={i18n} />
}

Expand Down
23 changes: 4 additions & 19 deletions src/wrapTWithDefaultNs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit b7b65bc

Please sign in to comment.