Skip to content

Commit a378208

Browse files
feat: dynamically generate search placeholder (freeCodeCamp#56276)
Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com>
1 parent 45fb377 commit a378208

12 files changed

+353
-25
lines changed

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
**/*fixtures*
33
api-server/lib
44
client/**/trending.json
5+
client/**/search-bar.json
56
client/config/*.json
67
client/config/browser-scripts/*.json
78
client/static

client/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ static/curriculum-data
1414
# Generated config
1515
config/browser-scripts/*.json
1616
i18n/locales/**/trending.json
17+
i18n/locales/**/search-bar.json
1718

1819
# Config
1920

client/i18n/config.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,25 @@ i18n.use(initReactI18next).init({
5353
if (clientLocale !== 'english') {
5454
module.exports = require('./locales/' + clientLocale + '/links.json');
5555
}
56+
`,
57+
'search-bar': preval`
58+
const envData = require('../config/env.json');
59+
const { clientLocale } = envData;
60+
if (clientLocale !== 'english') {
61+
module.exports = require('./locales/' + clientLocale + '/search-bar.json');
62+
}
5663
`
5764
},
5865
en: {
5966
translations: preval`module.exports = require('./locales/english/translations.json')`,
6067
trending: preval`module.exports = require('./locales/english/trending.json')`,
6168
intro: preval`module.exports = require('./locales/english/intro.json')`,
6269
metaTags: preval`module.exports = require('./locales/english/meta-tags.json')`,
63-
links: preval`module.exports = require('./locales/english/links.json')`
70+
links: preval`module.exports = require('./locales/english/links.json')`,
71+
'search-bar': preval`module.exports = require('./locales/english/search-bar.json')`
6472
}
6573
},
66-
ns: ['translations', 'trending', 'intro', 'metaTags', 'links'],
74+
ns: ['translations', 'trending', 'intro', 'metaTags', 'links', 'search-bar'],
6775
defaultNS: 'translations',
6876
returnObjects: true,
6977
// Uncomment the next line for debug logging

client/i18n/locales/english/translations.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,10 @@
692692
},
693693
"search": {
694694
"label": "Search",
695-
"placeholder": "Search 10,700+ tutorials",
695+
"placeholder": {
696+
"default": "Search our tutorials",
697+
"numbered": "Search {{ roundedTotalRecords }}+ tutorials"
698+
},
696699
"see-results": "See all results for {{searchQuery}}",
697700
"no-tutorials": "No tutorials found",
698701
"try": "Looking for something? Try the search bar on this page.",

client/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
"build": "NODE_OPTIONS=\"--max-old-space-size=7168\" gatsby build --prefix-paths",
2424
"build:scripts": "pnpm run -F=browser-scripts build",
2525
"clean": "gatsby clean",
26-
"common-setup": "pnpm -w run create:shared && pnpm run create:env && pnpm run create:trending",
26+
"common-setup": "pnpm -w run create:shared && pnpm run create:env && pnpm run create:trending && pnpm run create:search-placeholder",
2727
"create:env": "DEBUG=fcc:* ts-node ./tools/create-env.ts",
2828
"create:trending": "ts-node ./tools/download-trending.ts",
29+
"create:search-placeholder": "ts-node ./tools/generate-search-placeholder",
2930
"predevelop": "pnpm run common-setup && pnpm run build:scripts --env development",
3031
"develop": "NODE_OPTIONS=\"--max-old-space-size=7168\" gatsby develop --inspect=9230",
3132
"lint": "ts-node ./i18n/schema-validation.ts",
@@ -159,6 +160,7 @@
159160
"core-js": "2.6.12",
160161
"dotenv": "16.4.5",
161162
"gatsby-plugin-webpack-bundle-analyser-v2": "1.1.32",
163+
"i18next-fs-backend": "2.3.2",
162164
"jest-json-schema-extended": "1.0.1",
163165
"joi": "17.12.2",
164166
"js-yaml": "4.1.0",

client/src/components/search/searchBar/search-bar-optimized.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@ const SearchBarOptimized = ({
99
innerRef
1010
}: Pick<SearchBarProps, 'innerRef'>): JSX.Element => {
1111
const { t } = useTranslation();
12-
const placeholder = t('search.placeholder');
12+
// TODO: Refactor this fallback when all translation files are synced
13+
const searchPlaceholder = t('search-bar:placeholder').startsWith(
14+
'search.placeholder.'
15+
)
16+
? t('search.placeholder')
17+
: t('search-bar:placeholder');
1318
const searchUrl = searchPageUrl;
1419
const [value, setValue] = useState('');
1520
const inputElementRef = useRef<HTMLInputElement>(null);
@@ -50,7 +55,7 @@ const SearchBarOptimized = ({
5055
className='ais-SearchBox-input'
5156
maxLength={512}
5257
onChange={onChange}
53-
placeholder={placeholder}
58+
placeholder={searchPlaceholder}
5459
spellCheck='false'
5560
type='search'
5661
value={value}

client/src/components/search/searchBar/search-bar.tsx

+7-1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,12 @@ export class SearchBar extends Component<SearchBarProps, SearchBarState> {
201201
render(): JSX.Element {
202202
const { isDropdownEnabled, isSearchFocused, innerRef, t } = this.props;
203203
const { index } = this.state;
204+
// TODO: Refactor this fallback when all translation files are synced
205+
const searchPlaceholder = t('search-bar:placeholder').startsWith(
206+
'search.placeholder.'
207+
)
208+
? t('search.placeholder')
209+
: t('search-bar:placeholder');
204210

205211
return (
206212
<WithInstantSearch>
@@ -223,7 +229,7 @@ export class SearchBar extends Component<SearchBarProps, SearchBarState> {
223229
translations={{
224230
submitTitle: t('icons.magnifier'),
225231
resetTitle: t('icons.input-reset'),
226-
placeholder: t('search.placeholder')
232+
placeholder: searchPlaceholder
227233
}}
228234
onFocus={this.handleFocus}
229235
/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { clientLocale } from '../config/env.json';
2+
import {
3+
convertToLocalizedString,
4+
generateSearchPlaceholder,
5+
roundDownToNearestHundred
6+
} from './generate-search-placeholder';
7+
8+
describe('Search bar placeholder tests:', () => {
9+
describe('Number rounding', () => {
10+
test('Numbers less than 100 return 0', () => {
11+
const testArr = [0, 1, 50, 99];
12+
13+
testArr.forEach(num => {
14+
expect(roundDownToNearestHundred(num)).toEqual(0);
15+
});
16+
});
17+
18+
test('Numbers greater than 100 return a number rounded down to the nearest 100', () => {
19+
const testArr = [
20+
{
21+
num: 100,
22+
expected: 100
23+
},
24+
{
25+
num: 101,
26+
expected: 100
27+
},
28+
{
29+
num: 199,
30+
expected: 100
31+
},
32+
{
33+
num: 999,
34+
expected: 900
35+
},
36+
{
37+
num: 1000,
38+
expected: 1000
39+
},
40+
{
41+
num: 1001,
42+
expected: 1000
43+
},
44+
{
45+
num: 1999,
46+
expected: 1900
47+
},
48+
{
49+
num: 10000,
50+
expected: 10000
51+
},
52+
{
53+
num: 10001,
54+
expected: 10000
55+
},
56+
{
57+
num: 19999,
58+
expected: 19900
59+
}
60+
];
61+
62+
testArr.forEach(obj => {
63+
expect(roundDownToNearestHundred(obj.num)).toEqual(obj.expected);
64+
});
65+
});
66+
});
67+
68+
describe('Number formatting', () => {
69+
test('Numbers are converted to the correct decimal or comma format for each locale', () => {
70+
const testArr = [
71+
{
72+
num: 100,
73+
locale: 'en',
74+
expected: '100'
75+
},
76+
{
77+
num: 100,
78+
locale: 'zh',
79+
expected: '100'
80+
},
81+
{
82+
num: 100,
83+
locale: 'de',
84+
expected: '100'
85+
},
86+
{
87+
num: 1000,
88+
locale: 'en',
89+
expected: '1,000'
90+
},
91+
{
92+
num: 1000,
93+
locale: 'zh',
94+
expected: '1,000'
95+
},
96+
{
97+
num: 1000,
98+
locale: 'de',
99+
expected: '1.000'
100+
},
101+
{
102+
num: 10000,
103+
locale: 'en',
104+
expected: '10,000'
105+
},
106+
{
107+
num: 10000,
108+
locale: 'zh',
109+
expected: '10,000'
110+
},
111+
{
112+
num: 10000,
113+
locale: 'de',
114+
expected: '10.000'
115+
},
116+
{
117+
num: 100000,
118+
locale: 'en',
119+
expected: '100,000'
120+
},
121+
{
122+
num: 100000,
123+
locale: 'zh',
124+
expected: '100,000'
125+
},
126+
{
127+
num: 100000,
128+
locale: 'de',
129+
expected: '100.000'
130+
}
131+
];
132+
133+
testArr.forEach(obj => {
134+
const { num, locale, expected } = obj;
135+
expect(convertToLocalizedString(num, locale)).toEqual(expected);
136+
});
137+
});
138+
});
139+
140+
// Note: Only test the English locale to prevent duplicate tests,
141+
// and just to ensure the logic is working as expected.
142+
if (clientLocale === 'english') {
143+
describe('Placeholder strings', () => {
144+
test('When the total number of hits is less than 100 the expected placeholder is generated', async () => {
145+
const expected = 'Search our tutorials';
146+
const placeholderText = await generateSearchPlaceholder({
147+
mockRecordsNum: 99,
148+
locale: 'english'
149+
});
150+
151+
expect(placeholderText).toEqual(expected);
152+
});
153+
154+
test('When the total number of hits is equal to 100 the expected placeholder is generated', async () => {
155+
const placeholderText = await generateSearchPlaceholder({
156+
mockRecordsNum: 100,
157+
locale: 'english'
158+
});
159+
const expected = 'Search 100+ tutorials';
160+
161+
expect(placeholderText).toEqual(expected);
162+
});
163+
164+
test('When the total number of hits is greater than 100 the expected placeholder is generated', async () => {
165+
const placeholderText = await generateSearchPlaceholder({
166+
mockRecordsNum: 11000,
167+
locale: 'english'
168+
});
169+
const expected = 'Search 11,000+ tutorials';
170+
171+
expect(placeholderText).toEqual(expected);
172+
});
173+
});
174+
}
175+
});

0 commit comments

Comments
 (0)