Skip to content

Commit

Permalink
feat(gatsby-plugin-manifest): add i18n, localization (#13471)
Browse files Browse the repository at this point in the history
* 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 <cansrau@gmail.com>

* fix(gatsby-plugin-manifest): make find callback prop more readable

Co-Authored-By: CanRau <cansrau@gmail.com>

* fix(gatsby-plugin-manifest): make find callback prop more readable

Co-Authored-By: CanRau <cansrau@gmail.com>

* docs(gatsby-plugin-manifest): decrease config header level

Co-Authored-By: CanRau <cansrau@gmail.com>

* 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
  • Loading branch information
CanRau authored and freiksenet committed Jul 2, 2019
1 parent 5bedc01 commit d93e478
Show file tree
Hide file tree
Showing 7 changed files with 402 additions and 48 deletions.
51 changes: 49 additions & 2 deletions packages/gatsby-plugin-manifest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ This plugin provides several features beyond manifest configuration to make your
- [Favicon support](https://www.w3.org/2005/10/howto-favicon)
- Legacy icon support (iOS)[^1]
- [Cache busting](https://www.keycdn.com/support/what-is-cache-busting)
- Localization - Provides unqiue manifests for path-based localization ([Gatsby Example](https://github.com/gatsbyjs/gatsby/tree/master/examples/using-i18n))

Each of these features has extensive configuration available so you're always in control.
Each of these features has extensive configuration available so you are always in control.

## Install

Expand Down Expand Up @@ -56,18 +57,21 @@ There are three modes in which icon generation can function: automatic, hybrid,
- Favicon - yes
- Legacy icon support - yes
- Cache busting - yes
- Localization - optional

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

- Favicon - yes
- Legacy icon support - yes
- Cache busting - yes
- Localization - optional

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

- Favicon - never
- Legacy icon support - yes
- Cache busting - never
- Localization - optional

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

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

### Feature configuration - **Optional**

#### Localization configuration

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`)

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.

`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.

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)

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.

```js
// in gatsby-config.js
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-manifest`,
options: {
name: `The Cool Application`,
short_name: `Cool App`,
description: `The application does cool things and makes your life better.`,
lang: `en`,
display: `standalone`,
icon: `src/images/icon.png`,
start_url: `/`,
background_color: `#663399`,
theme_color: `#fff`,
localize: [
{
start_url: `/de/`,
lang: `de`,
name: `Die coole Anwendung`,
short_name: `Coole Anwendung`,
description: `Die Anwendung macht coole Dinge und macht Ihr Leben besser.`,
},
],
},
},
],
}
```

#### Iterative icon options

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.
Expand Down Expand Up @@ -224,7 +271,7 @@ Cache busting works by calculating a unique "digest" of the provided icon and mo

- **\`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`

- **\`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)
- **\`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.

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

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\\"}]}"`;

exports[`Test plugin manifest options does file name based cache busting 1`] = `
[MockFunction] {
"calls": Array [
Array [
"public/manifest.webmanifest",
"{\\"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\\"}]}",
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,104 @@ Array [
]
`;

exports[`gatsby-plugin-manifest Manifest Link Generation Adds correct (default) i18n "manifest" link to head 1`] = `
Array [
<link
href="/manifest.webmanifest"
rel="manifest"
/>,
<link
href="/icons/icon-48x48.png"
rel="apple-touch-icon"
sizes="48x48"
/>,
<link
href="/icons/icon-72x72.png"
rel="apple-touch-icon"
sizes="72x72"
/>,
<link
href="/icons/icon-96x96.png"
rel="apple-touch-icon"
sizes="96x96"
/>,
<link
href="/icons/icon-144x144.png"
rel="apple-touch-icon"
sizes="144x144"
/>,
<link
href="/icons/icon-192x192.png"
rel="apple-touch-icon"
sizes="192x192"
/>,
<link
href="/icons/icon-256x256.png"
rel="apple-touch-icon"
sizes="256x256"
/>,
<link
href="/icons/icon-384x384.png"
rel="apple-touch-icon"
sizes="384x384"
/>,
<link
href="/icons/icon-512x512.png"
rel="apple-touch-icon"
sizes="512x512"
/>,
]
`;

exports[`gatsby-plugin-manifest Manifest Link Generation Adds correct (es) i18n "manifest" link to head 1`] = `
Array [
<link
href="/manifest_es.webmanifest"
rel="manifest"
/>,
<link
href="/icons/icon-48x48.png"
rel="apple-touch-icon"
sizes="48x48"
/>,
<link
href="/icons/icon-72x72.png"
rel="apple-touch-icon"
sizes="72x72"
/>,
<link
href="/icons/icon-96x96.png"
rel="apple-touch-icon"
sizes="96x96"
/>,
<link
href="/icons/icon-144x144.png"
rel="apple-touch-icon"
sizes="144x144"
/>,
<link
href="/icons/icon-192x192.png"
rel="apple-touch-icon"
sizes="192x192"
/>,
<link
href="/icons/icon-256x256.png"
rel="apple-touch-icon"
sizes="256x256"
/>,
<link
href="/icons/icon-384x384.png"
rel="apple-touch-icon"
sizes="384x384"
/>,
<link
href="/icons/icon-512x512.png"
rel="apple-touch-icon"
sizes="512x512"
/>,
]
`;

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`] = `
Array [
<link
Expand Down
94 changes: 87 additions & 7 deletions packages/gatsby-plugin-manifest/src/__tests__/gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ jest.mock(`fs`, () => {
/*
* We mock sharp because it depends on fs implementation (which is mocked)
* this causes test failures, so mock it to avoid
*
*/

jest.mock(`sharp`, () => {
Expand Down Expand Up @@ -229,11 +230,8 @@ describe(`Test plugin manifest options`, () => {
...pluginSpecificOptions,
})

expect(sharp).toHaveBeenCalledTimes(3)
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify(manifestOptions)
)
expect(sharp).toHaveBeenCalledTimes(2)
expect(fs.writeFileSync).toMatchSnapshot()
})

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

expect(sharp).toHaveBeenCalledTimes(3)
expect(sharp).toHaveBeenCalledTimes(2)
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify(manifestOptions)
Expand All @@ -270,9 +268,91 @@ describe(`Test plugin manifest options`, () => {
...pluginSpecificOptions,
})

expect(sharp).toHaveBeenCalledTimes(3)
expect(sharp).toHaveBeenCalledTimes(2)
const content = JSON.parse(fs.writeFileSync.mock.calls[0][1])
expect(content.icons[0].purpose).toEqual(`all`)
expect(content.icons[1].purpose).toEqual(`maskable`)
})

it(`generates all language versions`, async () => {
fs.statSync.mockReturnValueOnce({ isFile: () => true })
const pluginSpecificOptions = {
localize: [
{
...manifestOptions,
start_url: `/de/`,
lang: `de`,
},
{
...manifestOptions,
start_url: `/es/`,
lang: `es`,
},
{
...manifestOptions,
start_url: `/`,
},
],
}
const { localize, ...manifest } = pluginSpecificOptions
const expectedResults = localize.concat(manifest).map(x => {
return { ...manifest, ...x }
})

await onPostBootstrap(apiArgs, pluginSpecificOptions)

expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify(expectedResults[0])
)
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify(expectedResults[1])
)
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify(expectedResults[2])
)
})

it(`merges default and language options`, async () => {
fs.statSync.mockReturnValueOnce({ isFile: () => true })
const pluginSpecificOptions = {
...manifestOptions,
localize: [
{
start_url: `/de/`,
lang: `de`,
},
{
start_url: `/es/`,
lang: `es`,
},
],
}
const { localize, ...manifest } = pluginSpecificOptions
const expectedResults = localize
.concat(manifest)
.map(({ language, manifest }) => {
return {
...manifestOptions,
...manifest,
}
})

await onPostBootstrap(apiArgs, pluginSpecificOptions)

expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify(expectedResults[0])
)
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify(expectedResults[1])
)
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.anything(),
JSON.stringify(expectedResults[2])
)
})
})
33 changes: 33 additions & 0 deletions packages/gatsby-plugin-manifest/src/__tests__/gatsby-ssr.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const setHeadComponents = args => (headComponents = headComponents.concat(args))

const ssrArgs = {
setHeadComponents,
pathname: `/`,
}

describe(`gatsby-plugin-manifest`, () => {
Expand Down Expand Up @@ -91,6 +92,38 @@ describe(`gatsby-plugin-manifest`, () => {
})
expect(headComponents).toMatchSnapshot()
})

const i18nArgs = [
{
...ssrArgs,
pathname: `/about-us`,
testName: `Adds correct (default) i18n "manifest" link to head`,
},
{
...ssrArgs,
pathname: `/es/sobre-nosotros`,
testName: `Adds correct (es) i18n "manifest" link to head`,
},
]

i18nArgs.forEach(({ testName, ...args }) =>
it(testName, () => {
onRenderBody(args, {
start_url: `/`,
localize: [
{
start_url: `/de/`,
lang: `de`,
},
{
start_url: `/es/`,
lang: `es`,
},
],
})
expect(headComponents).toMatchSnapshot()
})
)
})

describe(`Legacy Icons`, () => {
Expand Down
Loading

0 comments on commit d93e478

Please sign in to comment.