Skip to content

Commit

Permalink
feat: Allow customize message for missing translations (#255)
Browse files Browse the repository at this point in the history
  • Loading branch information
tricoder42 authored Aug 8, 2018
1 parent afdc03c commit c5ad5ac
Show file tree
Hide file tree
Showing 12 changed files with 118 additions and 14 deletions.
26 changes: 26 additions & 0 deletions docs/ref/core.rst
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,32 @@ Reference
// const i18n = setupI18n()
// i18n.load(catalogs)
.. js:attribute:: options.missing

Custom message to be returned when translation is missing. This is useful for
debugging:

.. code-block:: jsx
import { setupI18n } from "@lingui/core"
const i18n = setupI18n({ missing: "🚨" })
i18n._('missing translation') === "🚨"
This might be also a function which is called with active language and message ID:

.. code-block:: jsx
import { setupI18n } from "@lingui/core"
function missing(language, id) {
alert(`Translation in ${language} for ${id} is missing!`)
return id
}
const i18n = setupI18n({ missing })
i18n._('missing translation') // raises alert
.. js:class:: I18n

Constructor for I18n class isn't exported from the package. Instead, always use
Expand Down
19 changes: 19 additions & 0 deletions docs/ref/react.rst
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ I18nProvider
:prop string|string[] locales: List of locales used for date/number formatting. Defaults to active language.
:prop object catalogs: Message catalogs
:prop React.Element|React.Class|string defaultRender: Default element to render translation
:prop string|Function missing: Custom message to be returned when translation is missing

``defaultRender`` has the same meaning as ``render`` in other i18n
components. :ref:`Rendering of translations <rendering-translations>` is explained
Expand All @@ -356,6 +357,24 @@ at the beginning of this document.
different formats for the same language (e.g. arabic numerals have several
representations).

``missing`` is used as a default translation when translation is missing. It might
be also a function, which is called with language and message ID. This is useful
for debugging:

.. code-block:: jsx
import React from 'react';
import { I18nProvider } from '@lingui/react';
const App = ({ language} ) => {
return (
<I18nProvider language={language} missing="🚨">
{/* This will render as 🚨*/}
<Trans id="missing translation" />
</I18nProvider>
);
}
``catalogs`` is a type of ``Catalogs``:

.. code-block:: jsx
Expand Down
9 changes: 3 additions & 6 deletions packages/cli/src/api/formats/lingui.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,13 +133,10 @@ export default (config: LinguiConfig): CatalogFormat => ({
},

getLocale(filename) {
const filenameRe = new RegExp(
this.formatFilename(sourceFilename, "([^/]+)")
)
const match = filenameRe.exec(filename)
if (!match) return null
const [messages, locale] = filename.split(path.sep).reverse()

return match[1]
if (messages !== "messages.json" || !locales.isValid(locale)) return null
return locale
},

getLocales() {
Expand Down
12 changes: 9 additions & 3 deletions packages/cli/src/api/formats/lingui.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,15 @@ describe("Catalog formats - lingui", function() {
describe("getLocale", function() {
it("should get locale for given filename", function() {
const config = createConfig()
expect(plugin(config).getLocale("en/messages.json")).toEqual("en")
expect(plugin(config).getLocale("en_US/messages.json")).toEqual("en_US")
expect(plugin(config).getLocale("en-US/messages.json")).toEqual("en-US")
expect(
plugin(config).getLocale(path.join("en", "messages.json"))
).toEqual("en")
expect(
plugin(config).getLocale(path.join("en_US", "messages.json"))
).toEqual("en_US")
expect(
plugin(config).getLocale(path.join("en-US", "messages.json"))
).toEqual("en-US")
})

it("should return null for invalid locales", function() {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/api/formats/utils/locales.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export function isValid(locale: string): boolean {
* @return {LocaleInfo}
*/
export function parse(locale: string): ?LocaleInfo {
if (typeof locale !== "string") return null

const schema = bcp47.parse(locale.replace("_", "-"))
if (!schema.language) return null

Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ type Catalogs = { [key: string]: Catalog }
type setupI18nProps = {
language?: string,
locales?: Locales,
catalogs?: Catalogs
catalogs?: Catalogs,
missing?: string | Function
}

function getLanguageData(catalog) {
Expand All @@ -46,6 +47,8 @@ class I18n {
// Message catalogs
_catalogs: Catalogs

_missing: ?string | Function

// Messages/language data in active language.
// This is optimization, so we don't perform object lookup
// _catalogs[language] for each translation.
Expand Down Expand Up @@ -178,6 +181,13 @@ class I18n {
) {
let translation = this.messages[id] || defaults || id

// replace missing messages with custom message for debugging
const missing = this._missing
if (!this.messages[id] && missing) {
translation =
typeof missing === "function" ? missing(this.language, id) : missing
}

if (process.env.NODE_ENV !== "production") {
if (isString(translation) && this._dev && isFunction(this._dev.compile)) {
translation = this._dev.compile(translation)
Expand Down Expand Up @@ -211,6 +221,7 @@ function setupI18n(params?: setupI18nProps = {}): I18n {

if (params.catalogs) i18n.load(params.catalogs)
if (params.language) i18n.activate(params.language, params.locales)
if (params.missing) i18n._missing = params.missing

return i18n
}
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/i18n.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,4 +264,31 @@ describe("I18n", function() {
)
})
})

describe("params.missing - handling missing translations", function() {
it("._ should return custom string for missing translations", function() {
const i18n = setupI18n({
missing: "xxx",
language: "en",
catalogs: { en: { messages: { exists: "exists" } } }
})
expect(i18n._("exists")).toEqual("exists")
expect(i18n._("missing")).toEqual("xxx")
})

it("._ should call a function with message ID of missing translation", function() {
const missing = jest.fn((language, id) =>
id
.split("")
.reverse()
.join("")
)
const i18n = setupI18n({
language: "en",
missing
})
expect(i18n._("missing")).toEqual("gnissim")
expect(missing).toHaveBeenCalledWith("en", "missing")
})
})
})
2 changes: 1 addition & 1 deletion packages/core/src/i18nMark.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { i18nMark } from "./index"
import { i18nMark } from "@lingui/core"

describe("i18nMark", function() {
it("should be identity function (removed by babel extract plugin)", function() {
Expand Down
6 changes: 5 additions & 1 deletion packages/react/src/I18nProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type I18nProviderProps = {
locales?: Locales,
catalogs?: Catalogs,
i18n?: I18n,
missing?: string | Function,

defaultRender: ?any
}
Expand Down Expand Up @@ -72,7 +73,7 @@ export default class I18nProvider extends React.Component<I18nProviderProps> {

constructor(props: I18nProviderProps) {
super(props)
const { language, locales, catalogs } = props
const { language, locales, catalogs, missing } = props
const i18n =
props.i18n ||
setupI18n({
Expand All @@ -81,6 +82,7 @@ export default class I18nProvider extends React.Component<I18nProviderProps> {
catalogs
})
this.linguiPublisher = new LinguiPublisher(i18n)
this.linguiPublisher.i18n._missing = this.props.missing
}

componentDidUpdate(prevProps: I18nProviderProps) {
Expand All @@ -92,6 +94,8 @@ export default class I18nProvider extends React.Component<I18nProviderProps> {
) {
this.linguiPublisher.update({ language, catalogs, locales })
}

this.linguiPublisher.i18n._missing = this.props.missing
}

getChildContext() {
Expand Down
11 changes: 10 additions & 1 deletion packages/react/src/I18nProvider.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from "react"
import { shallow, mount } from "enzyme"

import { setupI18n } from "@lingui/core"
import { I18nProvider } from "@lingui/react"
import { I18nProvider, Trans } from "@lingui/react"
import { LinguiPublisher } from "./I18nProvider"
import { mockConsole } from "./mocks"

Expand Down Expand Up @@ -119,6 +119,15 @@ describe("I18nProvider", function() {
node.setProps({ locales: "cs-CZ" })
expect(listener).toBeCalled()
})

it("should render custom message for missing translation", function() {
const text = node =>
mount(<I18nProvider missing="xxx">{node}</I18nProvider>)
.find("Render")
.text()
const translation = text(<Trans id="missing" />)
expect(translation).toEqual("xxx")
})
})

describe("I18nPublisher", function() {
Expand Down
2 changes: 1 addition & 1 deletion packages/react/src/Trans.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ describe("Trans component", function() {
)
})

it.only("should render component in variables", function() {
it("should render component in variables", function() {
const translation = html(
<Trans id="Hello {name}" values={{ name: <strong>John</strong> }} />
)
Expand Down
3 changes: 3 additions & 0 deletions scripts/build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ function shouldSkipBundle(bundle, bundleType) {
}

async function copyFlowTypes(name) {
// Windows isn't supported in flow gen-flow-files
if (process.platform === "win32") return

const srcDir = `packages/${name}/src`
const outDir = `build/packages/${name}`

Expand Down

0 comments on commit c5ad5ac

Please sign in to comment.