Skip to content

Commit 7ae6e95

Browse files
Grab first available language as default if one isn't set, localize missing translation fallbacks (#110)
* setting up getOptions to return the first lagnuage available as default if a default language isn't provided * set up the missing translation callback to localize the translation before passing it as a value * prettier and fixing some flow issues with onMissingTranslation fixes * Import ComponentClass and Component (#106) Both ComponentClass and Component should be imported from `react`. `react-redux` doesn't contain those two. * update CHANGELOG * 3.2.2 * move onMissingTranslation logic from getLocalizedElement to getTranslate * fix flow issues * remove uneeded import from TS definition * add missing default translation message
1 parent 4f045e3 commit 7ae6e95

File tree

7 files changed

+109
-81
lines changed

7 files changed

+109
-81
lines changed

.flowconfig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@
1212
[options]
1313
module.ignore_non_literal_requires=true
1414
module.name_mapper='react-localize-redux' -> '<PROJECT_ROOT>/src/index.js'
15+
suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe

src/Translate.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,10 @@ class WrappedTranslate extends React.Component<TranslateWithContextProps> {
8383
const translation = renderToStaticMarkup(children);
8484
context.addTranslationForLanguage &&
8585
context.addTranslationForLanguage(
86-
{ [id]: translation },
86+
{
87+
// $FlowFixMe: flow complains that type of id can't be undefined, but we already guard against this in the checks above
88+
[id]: translation
89+
},
8790
defaultLanguage
8891
);
8992
}

src/index.d.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import {
55
ComponentType
66
} from 'react';
77
import { Store } from 'redux';
8-
import { ComponentClass, Component } from 'react';
98

109
export as namespace ReactLocalizeRedux;
1110

@@ -32,7 +31,7 @@ type TransFormFunction = (
3231
type MissingTranslationOptions = {
3332
translationId: string;
3433
languageCode: string;
35-
defaultTranslation: string;
34+
defaultTranslation: LocalizedElement;
3635
};
3736

3837
export type onMissingTranslationFunction = (

src/localize.js

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,22 @@ export type InitializeOptions = {
5252
ignoreTranslateChildren?: boolean
5353
};
5454

55+
// This is to get around the whole default options issue with Flow
56+
// I tried using the $Diff approach, but with no luck so for now stuck with this terd.
57+
// Because sometimes you just want flow to shut up!
58+
type InitializeOptionsRequired = {
59+
renderToStaticMarkup: renderToStaticMarkupFunction | false,
60+
renderInnerHtml: boolean,
61+
onMissingTranslation: onMissingTranslationFunction,
62+
defaultLanguage: string,
63+
ignoreTranslateChildren: boolean
64+
};
65+
5566
export type TranslateOptions = {
5667
language?: string,
5768
renderInnerHtml?: boolean,
5869
onMissingTranslation?: onMissingTranslationFunction,
70+
defaultLanguage?: string,
5971
ignoreTranslateChildren?: boolean
6072
};
6173

@@ -66,7 +78,7 @@ export type AddTranslationOptions = {
6678
export type LocalizeState = {
6779
+languages: Language[],
6880
+translations: Translations,
69-
+options: InitializeOptions
81+
+options: InitializeOptionsRequired
7082
};
7183

7284
export type TranslatedLanguage = {
@@ -102,7 +114,7 @@ export type MultipleLanguageTranslation = {
102114
type MissingTranslationOptions = {
103115
translationId: string,
104116
languageCode: string,
105-
defaultTranslation: string
117+
defaultTranslation: LocalizedElement
106118
};
107119

108120
export type onMissingTranslationFunction = (
@@ -274,9 +286,9 @@ export function translations(
274286
}
275287

276288
export function options(
277-
state: InitializeOptions = defaultTranslateOptions,
289+
state: InitializeOptionsRequired = defaultTranslateOptions,
278290
action: ActionDetailed
279-
): InitializeOptions {
291+
): InitializeOptionsRequired {
280292
switch (action.type) {
281293
case INITIALIZE:
282294
const options: any = action.payload.options || {};
@@ -288,10 +300,11 @@ export function options(
288300
}
289301
}
290302

291-
export const defaultTranslateOptions: InitializeOptions = {
303+
export const defaultTranslateOptions: InitializeOptionsRequired = {
292304
renderToStaticMarkup: false,
293305
renderInnerHtml: false,
294306
ignoreTranslateChildren: false,
307+
defaultLanguage: '',
295308
onMissingTranslation: ({ translationId, languageCode }) =>
296309
'Missing translationId: ${ translationId } for language: ${ languageCode }'
297310
};
@@ -361,8 +374,20 @@ export const getTranslations = (state: LocalizeState): Translations => {
361374
export const getLanguages = (state: LocalizeState): Language[] =>
362375
state.languages;
363376

364-
export const getOptions = (state: LocalizeState): InitializeOptions =>
365-
state.options;
377+
export const getOptions = (state: LocalizeState): InitializeOptionsRequired => {
378+
const options = Object.assign({}, state.options);
379+
let languages;
380+
381+
// If there isn't a default language, grab the first languages from the
382+
// available languages as default
383+
384+
if (!options.defaultLanguage) {
385+
languages = getLanguages(state) || [];
386+
options.defaultLanguage = languages[0] ? languages[0].code : '';
387+
}
388+
389+
return options;
390+
};
366391

367392
export const getActiveLanguage = (state: LocalizeState): Language => {
368393
const languages = getLanguages(state);
@@ -452,43 +477,60 @@ export const getTranslate: Selector<
452477
: translationsForActiveLanguage;
453478

454479
const defaultTranslations =
455-
activeLanguage &&
456-
activeLanguage.code === initializeOptions.defaultLanguage
480+
activeLanguage && activeLanguage.code === defaultLanguage
457481
? translationsForActiveLanguage
458-
: initializeOptions.defaultLanguage !== undefined
459-
? getTranslationsForLanguage(initializeOptions.defaultLanguage)
460-
: {};
482+
: getTranslationsForLanguage(defaultLanguage);
461483

462484
const languageCode =
463485
overrideLanguage !== undefined
464486
? overrideLanguage
465487
: activeLanguage && activeLanguage.code;
466488

467-
const onMissingTranslation = (translationId: string) => {
468-
return mergedOptions.onMissingTranslation({
469-
translationId,
489+
const mergedOptions = { ...defaultOptions, ...translateOptions };
490+
491+
const getTranslation = (translationId: string) => {
492+
const hasValidTranslation = translations[translationId] !== undefined;
493+
const hasValidDefaultTranslation =
494+
defaultTranslations[translationId] !== undefined;
495+
496+
const defaultTranslation = hasValidDefaultTranslation
497+
? getLocalizedElement({
498+
translation: defaultTranslations[translationId],
499+
data,
500+
renderInnerHtml: mergedOptions.renderInnerHtml
501+
})
502+
: "No default translation found! Ensure you've added translations for your default langauge.";
503+
504+
// if translation is not valid then generate the on missing translation message in it's place
505+
const translation = hasValidTranslation
506+
? translations[translationId]
507+
: mergedOptions.onMissingTranslation({
508+
translationId,
509+
languageCode,
510+
defaultTranslation
511+
});
512+
513+
// if translations are missing than ovrride data to include translationId, languageCode
514+
// as these will be needed to render missing translations message
515+
const translationData = hasValidTranslation
516+
? data
517+
: { translationId, languageCode };
518+
519+
return getLocalizedElement({
520+
translation,
521+
data: translationData,
470522
languageCode,
471-
defaultTranslation: defaultTranslations[translationId]
523+
renderInnerHtml: mergedOptions.renderInnerHtml
472524
});
473525
};
474526

475-
const mergedOptions = { ...defaultOptions, ...translateOptions };
476-
const { renderInnerHtml } = mergedOptions;
477-
const sharedParams = {
478-
translations,
479-
data,
480-
languageCode,
481-
renderInnerHtml,
482-
onMissingTranslation
483-
};
484-
485527
if (typeof value === 'string') {
486-
return getLocalizedElement({ translationId: value, ...sharedParams });
528+
return getTranslation(value);
487529
} else if (Array.isArray(value)) {
488530
return value.reduce((prev, cur) => {
489531
return {
490532
...prev,
491-
[cur]: getLocalizedElement({ translationId: cur, ...sharedParams })
533+
[cur]: getTranslation(cur)
492534
};
493535
}, {});
494536
} else {

src/utils.js

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,17 @@ import type {
1616
} from './localize';
1717

1818
type LocalizedElementOptions = {
19-
translationId: string,
20-
translations: TranslatedLanguage,
19+
translation: string,
2120
data: TranslatePlaceholderData,
22-
languageCode: string,
23-
renderInnerHtml: boolean,
24-
onMissingTranslation: (translationId: string) => string
21+
renderInnerHtml: boolean
2522
};
2623

2724
export const getLocalizedElement = (
2825
options: LocalizedElementOptions
2926
): LocalizedElement => {
30-
const {
31-
translationId,
32-
translations,
33-
data,
34-
renderInnerHtml,
35-
onMissingTranslation
36-
} = options;
37-
const localizedString =
38-
translations[translationId] || onMissingTranslation(translationId);
39-
const placeholderData = translations[translationId]
40-
? data
41-
: {
42-
translationId: options.translationId,
43-
languageCode: options.languageCode
44-
};
45-
const translatedValueOrArray = templater(localizedString, placeholderData);
27+
const { translation, data, renderInnerHtml } = options;
28+
29+
const translatedValueOrArray = templater(translation, data);
4630

4731
// if result of templater is string, do the usual stuff
4832
if (typeof translatedValueOrArray === 'string') {
@@ -150,7 +134,7 @@ export const validateOptions = (
150134
typeof options.renderToStaticMarkup !== 'function'
151135
) {
152136
throw new Error(`
153-
react-localize-redux: initialize option renderToStaticMarkup is invalid.
137+
react-localize-redux: initialize option renderToStaticMarkup is invalid.
154138
Please see https://ryandrewjohnson.github.io/react-localize-docs/#initialize.
155139
`);
156140
}

tests/localize.test.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Adapter from 'enzyme-adapter-react-16';
33
import {
44
languages,
55
translations,
6+
getOptions,
67
getActiveLanguage,
78
getTranslationsForActiveLanguage,
89
getTranslationsForSpecificLanguage,
@@ -658,7 +659,8 @@ describe('localize', () => {
658659
bye: ['bye-en', 'bye-fr'],
659660
yo: ['yo ${ name }', 'yo-fr ${ name }'],
660661
foo: ['foo ${ bar }', 'foo-fr ${ bar }'],
661-
html: ['<b>hi-en</b>', '<b>hi-fr</b>']
662+
html: ['<b>hi-en</b>', '<b>hi-fr</b>'],
663+
money_no_translation: ['save $${ amount }']
662664
},
663665
options: defaultTranslateOptions
664666
};
@@ -749,6 +751,19 @@ describe('localize', () => {
749751
);
750752
});
751753

754+
it('should set first language available as default when no default is set', () => {
755+
const options = getOptions(state);
756+
expect(options.defaultLanguage).toBe(state.languages[0].code);
757+
});
758+
759+
it('should return value using default language when missing a translation with USD hard coded into translation', () => {
760+
state.options.onMissingTranslation = ({ defaultTranslation }) => defaultTranslation;
761+
const key = 'money_no_translation';
762+
const translate = getTranslate(state);
763+
const result = translate([key], { amount: 100 });
764+
expect(result[key]).toBe('save $100')
765+
});
766+
752767
it('should return value from onMissingTranslation option override', () => {
753768
state.options.onMissingTranslation = ({ translationId, languageCode }) =>
754769
'${translationId} - ${languageCode}';

tests/utils.test.js

Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,35 @@ describe('locale utils', () => {
1313
it('should return element with localized string', () => {
1414
const translations = { test: 'Here is my test' };
1515
const result = utils.getLocalizedElement({
16-
translationId: 'test',
17-
translations
16+
translation: 'Here is my test'
1817
});
1918
expect(result).toBe(translations.test);
2019
});
2120

2221
it('should render inner HTML when renderInnerHtml = true', () => {
23-
const translations = { test: '<h1>Here</h1> is my <strong>test</strong>' };
22+
const translation = '<h1>Here</h1> is my <strong>test</strong>';
2423
const wrapper = shallow(utils.getLocalizedElement({
25-
translationId: 'test',
26-
translations,
24+
translation,
2725
renderInnerHtml: true
2826
}));
2927

3028
expect(wrapper.find('span').exists()).toBe(true);
31-
expect(wrapper.html()).toEqual(`<span>${translations.test}</span>`);
29+
expect(wrapper.html()).toEqual(`<span>${translation}</span>`);
3230
});
3331

3432
it('should not render inner HTML when renderInnerHtml = false', () => {
35-
const translations = { test: '<h1>Here</h1> is my <strong>test</strong>' };
33+
const translation = '<h1>Here</h1> is my <strong>test</strong>';
3634
const result = utils.getLocalizedElement({
37-
translationId: 'test',
38-
translations,
35+
translation,
3936
renderInnerHtml: false
4037
});
41-
expect(result).toBe(translations.test);
42-
});
43-
44-
it('should return result of onMissingTranslation when translation = undefined', () => {
45-
const onMissingTranslation = () => 'My missing message';
46-
const result = utils.getLocalizedElement({
47-
translationId: 'nothing',
48-
translations: {},
49-
renderInnerHtml: true,
50-
onMissingTranslation
51-
});
52-
expect(result).toEqual('My missing message');
38+
expect(result).toBe(translation);
5339
});
5440

5541
it('should replace variables in translation string with data', () => {
56-
const translations = { test: 'Hello ${ name }' };
42+
const translation = 'Hello ${ name }';
5743
const result = utils.getLocalizedElement({
58-
translationId: 'test',
59-
translations,
44+
translation,
6045
renderInnerHtml: true,
6146
data: { name: 'Ted' }
6247
});
@@ -65,10 +50,9 @@ describe('locale utils', () => {
6550

6651
it('should handle React in data', () => {
6752
const Comp = () => <div>ReactJS</div>
68-
const translations = { test: 'Hello ${ comp } data' }
53+
const translation = 'Hello ${ comp } data';
6954
const result = utils.getLocalizedElement({
70-
translationId: 'test',
71-
translations,
55+
translation,
7256
data: { comp: <Comp /> }
7357
});
7458
expect(mount(result).text()).toContain('ReactJS');

0 commit comments

Comments
 (0)