Skip to content

Commit d93e478

Browse files
CanRaufreiksenet
authored andcommitted
feat(gatsby-plugin-manifest): add i18n, localization (#13471)
* feat(gatsby-plugin-manifest): add i18n, localization * feat(gatsby-plugin-manifest): update readme * fix(gatsby-plugin-manifest): make map callback prop more readable Co-Authored-By: CanRau <[email protected]> * fix(gatsby-plugin-manifest): make find callback prop more readable Co-Authored-By: CanRau <[email protected]> * fix(gatsby-plugin-manifest): make find callback prop more readable Co-Authored-By: CanRau <[email protected]> * docs(gatsby-plugin-manifest): decrease config header level Co-Authored-By: CanRau <[email protected]> * feat(gatsby-plugin-manifest): integrate suggestions by moonmeister language manifests in the `manifests` prop now merge top level options as suggested here #13471 (comment) use option merging style in README docs: change features as suggested here #13471 (comment) * feat(gatsby-plugin-manifest): incorporate suggestions * rename `manifests` to `localize` * rename `language` to `lang` * include `lang` in manifest file * merge root options and locales * ensure root only merges if it has start_url provided * always generate root options * remove regex * use start_url as matcher * update docs & test * docs(gatsby-plugin-manifest): merge moonmeisters description * feat(gatsby-plugin-manifest): merge moonmeisters naming suggestions * remove accidentally commited .patch file * fix: issue when makeManifest is run multiple times it concatenates the cache busting to file name. * feat: add basic caching so an icon isn't generated multiple times durring a single build. * fix: overriting manifest icons when using unique images for different locales. require name based cache busting fixes this issue the simplest. * refactor: modify code to only require name chache busting when a unique icon is specified for a locale in automatic mode * docs: update docs with link to i18n example * fix: tests and digest cache bug
1 parent 5bedc01 commit d93e478

File tree

7 files changed

+402
-48
lines changed

7 files changed

+402
-48
lines changed

packages/gatsby-plugin-manifest/README.md

+49-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ This plugin provides several features beyond manifest configuration to make your
99
- [Favicon support](https://www.w3.org/2005/10/howto-favicon)
1010
- Legacy icon support (iOS)[^1]
1111
- [Cache busting](https://www.keycdn.com/support/what-is-cache-busting)
12+
- Localization - Provides unqiue manifests for path-based localization ([Gatsby Example](https://github.com/gatsbyjs/gatsby/tree/master/examples/using-i18n))
1213

13-
Each of these features has extensive configuration available so you're always in control.
14+
Each of these features has extensive configuration available so you are always in control.
1415

1516
## Install
1617

@@ -56,18 +57,21 @@ There are three modes in which icon generation can function: automatic, hybrid,
5657
- Favicon - yes
5758
- Legacy icon support - yes
5859
- Cache busting - yes
60+
- Localization - optional
5961

6062
- Hybrid - Generate a manually configured set of icons from a single source icon.
6163

6264
- Favicon - yes
6365
- Legacy icon support - yes
6466
- Cache busting - yes
67+
- Localization - optional
6568

6669
- Manual - Don't generate or pre-configure any icons.
6770

6871
- Favicon - never
6972
- Legacy icon support - yes
7073
- Cache busting - never
74+
- Localization - optional
7175

7276
**_IMPORTANT:_** For best results, if you're providing an icon for generation it should be...
7377

@@ -132,6 +136,49 @@ In the manual mode, you are responsible for defining the entire web app manifest
132136

133137
### Feature configuration - **Optional**
134138

139+
#### Localization configuration
140+
141+
Localization allows you to create unique manifests for each localized version of your site. As many languages as you want are supported. Localization requires unique paths for each language (e.g. if your default about page is at `/about`, the german(`de`) version would be `/de/about`)
142+
143+
The default site language should be configured in your root plugin options. Any additional languages should be defined in the `localize` array. The root settings will be used as defaults if not overridden in a locale. Any configuration option available in the root is also available in the `localize` array.
144+
145+
`lang` and `start_url` are the only _required_ options in the array objects. `name`, `short_name`, and `description` are [recommended](https://www.w3.org/TR/appmanifest/#dfn-directionality-capable-members) to be translated if being used in the default language. All other config options are optional. This is helpful if you want to provide unique icons for each locale.
146+
147+
The [`lang` option](https://www.w3.org/TR/appmanifest/#lang-member) is part of the web app manifest specification and thus is required to be a [valid language tag](https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry)
148+
149+
Using localization requires name based cache busting when using a unique icon in automatic mode for a specific locale. This is automatically enabled if you provide and `icon` in a specific locale without uniquely defining `icons`. If you're using icon creation in hybrid or manual mode for your locales, rememmber to provide unique icon paths.
150+
151+
```js
152+
// in gatsby-config.js
153+
module.exports = {
154+
plugins: [
155+
{
156+
resolve: `gatsby-plugin-manifest`,
157+
options: {
158+
name: `The Cool Application`,
159+
short_name: `Cool App`,
160+
description: `The application does cool things and makes your life better.`,
161+
lang: `en`,
162+
display: `standalone`,
163+
icon: `src/images/icon.png`,
164+
start_url: `/`,
165+
background_color: `#663399`,
166+
theme_color: `#fff`,
167+
localize: [
168+
{
169+
start_url: `/de/`,
170+
lang: `de`,
171+
name: `Die coole Anwendung`,
172+
short_name: `Coole Anwendung`,
173+
description: `Die Anwendung macht coole Dinge und macht Ihr Leben besser.`,
174+
},
175+
],
176+
},
177+
},
178+
],
179+
}
180+
```
181+
135182
#### Iterative icon options
136183

137184
The `icon_options` object may be used to iteratively add configuration items to the `icons` array. Any options included in this object will be merged with each object of the `icons` array (custom or default). Key value pairs already in the `icons` array will take precedence over duplicate items in the `icon_options` array.
@@ -224,7 +271,7 @@ Cache busting works by calculating a unique "digest" of the provided icon and mo
224271

225272
- **\`query\`** - This is the default mode. File names are unmodified but a URL query is appended to all links. e.g. `icons/icon-48x48.png?digest=abc123`
226273

227-
- **\`name\`** - Changes the cache busting mode to be done by file name. File names and links are modified with the icon digest. e.g. `icons/icon-48x48-abc123.png` (only needed if your CDN does not support URL query based cache busting)
274+
- **\`name\`** - Changes the cache busting mode to be done by file name. File names and links are modified with the icon digest. e.g. `icons/icon-48x48-abc123.png` (only needed if your CDN does not support URL query based cache busting). This mode is required and automatically enabled for a locale's icons if you are providing a unique icon for a specific locale in automatic mode using the localization features.
228275

229276
- **\`none\`** - Disables cache busting. File names and links remain unmodified.
230277

Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`Test plugin manifest options correctly works with default parameters 1`] = `"{\\"name\\":\\"GatsbyJS\\",\\"short_name\\":\\"GatsbyJS\\",\\"start_url\\":\\"/\\",\\"background_color\\":\\"#f7f0eb\\",\\"theme_color\\":\\"#a2466c\\",\\"display\\":\\"standalone\\",\\"icons\\":[{\\"src\\":\\"icons/icon-48x48.png\\",\\"sizes\\":\\"48x48\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-72x72.png\\",\\"sizes\\":\\"72x72\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-96x96.png\\",\\"sizes\\":\\"96x96\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-144x144.png\\",\\"sizes\\":\\"144x144\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-192x192.png\\",\\"sizes\\":\\"192x192\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-256x256.png\\",\\"sizes\\":\\"256x256\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-384x384.png\\",\\"sizes\\":\\"384x384\\",\\"type\\":\\"image/png\\"},{\\"src\\":\\"icons/icon-512x512.png\\",\\"sizes\\":\\"512x512\\",\\"type\\":\\"image/png\\"}]}"`;
4+
5+
exports[`Test plugin manifest options does file name based cache busting 1`] = `
6+
[MockFunction] {
7+
"calls": Array [
8+
Array [
9+
"public/manifest.webmanifest",
10+
"{\\"name\\":\\"GatsbyJS\\",\\"short_name\\":\\"GatsbyJS\\",\\"start_url\\":\\"/\\",\\"background_color\\":\\"#f7f0eb\\",\\"theme_color\\":\\"#a2466c\\",\\"display\\":\\"standalone\\",\\"icons\\":[{\\"src\\":\\"icons/icon-48x48-contentDigest.png\\",\\"sizes\\":\\"48x48\\",\\"type\\":\\"image/png\\",\\"purpose\\":\\"all\\"},{\\"src\\":\\"icons/icon-128x128-contentDigest.png\\",\\"sizes\\":\\"128x128\\",\\"type\\":\\"image/png\\"}]}",
11+
],
12+
],
13+
"results": Array [
14+
Object {
15+
"type": "return",
16+
"value": undefined,
17+
},
18+
],
19+
}
20+
`;

packages/gatsby-plugin-manifest/src/__tests__/__snapshots__/gatsby-ssr.js.snap

+98
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,104 @@ Array [
492492
]
493493
`;
494494

495+
exports[`gatsby-plugin-manifest Manifest Link Generation Adds correct (default) i18n "manifest" link to head 1`] = `
496+
Array [
497+
<link
498+
href="/manifest.webmanifest"
499+
rel="manifest"
500+
/>,
501+
<link
502+
href="/icons/icon-48x48.png"
503+
rel="apple-touch-icon"
504+
sizes="48x48"
505+
/>,
506+
<link
507+
href="/icons/icon-72x72.png"
508+
rel="apple-touch-icon"
509+
sizes="72x72"
510+
/>,
511+
<link
512+
href="/icons/icon-96x96.png"
513+
rel="apple-touch-icon"
514+
sizes="96x96"
515+
/>,
516+
<link
517+
href="/icons/icon-144x144.png"
518+
rel="apple-touch-icon"
519+
sizes="144x144"
520+
/>,
521+
<link
522+
href="/icons/icon-192x192.png"
523+
rel="apple-touch-icon"
524+
sizes="192x192"
525+
/>,
526+
<link
527+
href="/icons/icon-256x256.png"
528+
rel="apple-touch-icon"
529+
sizes="256x256"
530+
/>,
531+
<link
532+
href="/icons/icon-384x384.png"
533+
rel="apple-touch-icon"
534+
sizes="384x384"
535+
/>,
536+
<link
537+
href="/icons/icon-512x512.png"
538+
rel="apple-touch-icon"
539+
sizes="512x512"
540+
/>,
541+
]
542+
`;
543+
544+
exports[`gatsby-plugin-manifest Manifest Link Generation Adds correct (es) i18n "manifest" link to head 1`] = `
545+
Array [
546+
<link
547+
href="/manifest_es.webmanifest"
548+
rel="manifest"
549+
/>,
550+
<link
551+
href="/icons/icon-48x48.png"
552+
rel="apple-touch-icon"
553+
sizes="48x48"
554+
/>,
555+
<link
556+
href="/icons/icon-72x72.png"
557+
rel="apple-touch-icon"
558+
sizes="72x72"
559+
/>,
560+
<link
561+
href="/icons/icon-96x96.png"
562+
rel="apple-touch-icon"
563+
sizes="96x96"
564+
/>,
565+
<link
566+
href="/icons/icon-144x144.png"
567+
rel="apple-touch-icon"
568+
sizes="144x144"
569+
/>,
570+
<link
571+
href="/icons/icon-192x192.png"
572+
rel="apple-touch-icon"
573+
sizes="192x192"
574+
/>,
575+
<link
576+
href="/icons/icon-256x256.png"
577+
rel="apple-touch-icon"
578+
sizes="256x256"
579+
/>,
580+
<link
581+
href="/icons/icon-384x384.png"
582+
rel="apple-touch-icon"
583+
sizes="384x384"
584+
/>,
585+
<link
586+
href="/icons/icon-512x512.png"
587+
rel="apple-touch-icon"
588+
sizes="512x512"
589+
/>,
590+
]
591+
`;
592+
495593
exports[`gatsby-plugin-manifest Manifest Link Generation Does not add a "theme color" meta tag if "theme_color_in_head" is set to false 1`] = `
496594
Array [
497595
<link

packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js

+87-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ jest.mock(`fs`, () => {
1010
/*
1111
* We mock sharp because it depends on fs implementation (which is mocked)
1212
* this causes test failures, so mock it to avoid
13+
*
1314
*/
1415

1516
jest.mock(`sharp`, () => {
@@ -229,11 +230,8 @@ describe(`Test plugin manifest options`, () => {
229230
...pluginSpecificOptions,
230231
})
231232

232-
expect(sharp).toHaveBeenCalledTimes(3)
233-
expect(fs.writeFileSync).toHaveBeenCalledWith(
234-
expect.anything(),
235-
JSON.stringify(manifestOptions)
236-
)
233+
expect(sharp).toHaveBeenCalledTimes(2)
234+
expect(fs.writeFileSync).toMatchSnapshot()
237235
})
238236

239237
it(`does not do cache cache busting`, async () => {
@@ -249,7 +247,7 @@ describe(`Test plugin manifest options`, () => {
249247
...pluginSpecificOptions,
250248
})
251249

252-
expect(sharp).toHaveBeenCalledTimes(3)
250+
expect(sharp).toHaveBeenCalledTimes(2)
253251
expect(fs.writeFileSync).toHaveBeenCalledWith(
254252
expect.anything(),
255253
JSON.stringify(manifestOptions)
@@ -270,9 +268,91 @@ describe(`Test plugin manifest options`, () => {
270268
...pluginSpecificOptions,
271269
})
272270

273-
expect(sharp).toHaveBeenCalledTimes(3)
271+
expect(sharp).toHaveBeenCalledTimes(2)
274272
const content = JSON.parse(fs.writeFileSync.mock.calls[0][1])
275273
expect(content.icons[0].purpose).toEqual(`all`)
276274
expect(content.icons[1].purpose).toEqual(`maskable`)
277275
})
276+
277+
it(`generates all language versions`, async () => {
278+
fs.statSync.mockReturnValueOnce({ isFile: () => true })
279+
const pluginSpecificOptions = {
280+
localize: [
281+
{
282+
...manifestOptions,
283+
start_url: `/de/`,
284+
lang: `de`,
285+
},
286+
{
287+
...manifestOptions,
288+
start_url: `/es/`,
289+
lang: `es`,
290+
},
291+
{
292+
...manifestOptions,
293+
start_url: `/`,
294+
},
295+
],
296+
}
297+
const { localize, ...manifest } = pluginSpecificOptions
298+
const expectedResults = localize.concat(manifest).map(x => {
299+
return { ...manifest, ...x }
300+
})
301+
302+
await onPostBootstrap(apiArgs, pluginSpecificOptions)
303+
304+
expect(fs.writeFileSync).toHaveBeenCalledWith(
305+
expect.anything(),
306+
JSON.stringify(expectedResults[0])
307+
)
308+
expect(fs.writeFileSync).toHaveBeenCalledWith(
309+
expect.anything(),
310+
JSON.stringify(expectedResults[1])
311+
)
312+
expect(fs.writeFileSync).toHaveBeenCalledWith(
313+
expect.anything(),
314+
JSON.stringify(expectedResults[2])
315+
)
316+
})
317+
318+
it(`merges default and language options`, async () => {
319+
fs.statSync.mockReturnValueOnce({ isFile: () => true })
320+
const pluginSpecificOptions = {
321+
...manifestOptions,
322+
localize: [
323+
{
324+
start_url: `/de/`,
325+
lang: `de`,
326+
},
327+
{
328+
start_url: `/es/`,
329+
lang: `es`,
330+
},
331+
],
332+
}
333+
const { localize, ...manifest } = pluginSpecificOptions
334+
const expectedResults = localize
335+
.concat(manifest)
336+
.map(({ language, manifest }) => {
337+
return {
338+
...manifestOptions,
339+
...manifest,
340+
}
341+
})
342+
343+
await onPostBootstrap(apiArgs, pluginSpecificOptions)
344+
345+
expect(fs.writeFileSync).toHaveBeenCalledWith(
346+
expect.anything(),
347+
JSON.stringify(expectedResults[0])
348+
)
349+
expect(fs.writeFileSync).toHaveBeenCalledWith(
350+
expect.anything(),
351+
JSON.stringify(expectedResults[1])
352+
)
353+
expect(fs.writeFileSync).toHaveBeenCalledWith(
354+
expect.anything(),
355+
JSON.stringify(expectedResults[2])
356+
)
357+
})
278358
})

packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js

+33
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const setHeadComponents = args => (headComponents = headComponents.concat(args))
1515

1616
const ssrArgs = {
1717
setHeadComponents,
18+
pathname: `/`,
1819
}
1920

2021
describe(`gatsby-plugin-manifest`, () => {
@@ -91,6 +92,38 @@ describe(`gatsby-plugin-manifest`, () => {
9192
})
9293
expect(headComponents).toMatchSnapshot()
9394
})
95+
96+
const i18nArgs = [
97+
{
98+
...ssrArgs,
99+
pathname: `/about-us`,
100+
testName: `Adds correct (default) i18n "manifest" link to head`,
101+
},
102+
{
103+
...ssrArgs,
104+
pathname: `/es/sobre-nosotros`,
105+
testName: `Adds correct (es) i18n "manifest" link to head`,
106+
},
107+
]
108+
109+
i18nArgs.forEach(({ testName, ...args }) =>
110+
it(testName, () => {
111+
onRenderBody(args, {
112+
start_url: `/`,
113+
localize: [
114+
{
115+
start_url: `/de/`,
116+
lang: `de`,
117+
},
118+
{
119+
start_url: `/es/`,
120+
lang: `es`,
121+
},
122+
],
123+
})
124+
expect(headComponents).toMatchSnapshot()
125+
})
126+
)
94127
})
95128

96129
describe(`Legacy Icons`, () => {

0 commit comments

Comments
 (0)