Skip to content

Commit 76dfa83

Browse files
committed
feat!: add trailing slash option
1 parent e62c6bb commit 76dfa83

File tree

12 files changed

+127
-154
lines changed

12 files changed

+127
-154
lines changed

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ Providing i18n and l10n to Gatsby. Besides translating pages and Markdown files,
5252
locale: `de-CH`,
5353
prefix: `de`,
5454
slugs: {
55-
'/products': '/produkte',
56-
'/products#donut-filled-with-jam': '/produkte#berliner',
57-
'/services/software-development': '/dienstleistungen/softwareentwicklung'
55+
'/products/': '/produkte/',
56+
'/products/#donut-filled-with-jam': '/produkte/#berliner',
57+
'/services/software-development/': '/dienstleistungen/softwareentwicklung/'
5858
},
59-
pageBlacklist: ['/do-not-translate-to-german'], // If there is a page with the a given path it won't be translated
59+
pageBlacklist: ['/do-not-translate-to-german/'], // If there is a page with the a given path it won't be translated
6060
messages: {
6161
"language": "Deutsch"
6262
},
@@ -65,8 +65,11 @@ Providing i18n and l10n to Gatsby. Besides translating pages and Markdown files,
6565
// omit certain path segments (relative directories)
6666
// be careful not to cause path collisions
6767
pathBlacklist: [
68-
'/pages' // /pages/products/gummibears becomes /products/gummibears
69-
]
68+
'/pages' // /pages/products/gummibears/ becomes /products/gummibears/
69+
],
70+
// behaves like https://www.gatsbyjs.com/docs/reference/config-files/gatsby-config/#trailingslash
71+
// default: always
72+
trailingSlash: 'always'
7073
},
7174
}
7275
```
@@ -84,7 +87,7 @@ With the built-in `<LanguageSwitcher>` component users can change between the lo
8487
`<LocalizedLink>` wraps Gatsby `<Link>` component, thus it should be possible to use it in the same way.
8588

8689
```jsx
87-
<LocalizedLink to="/products">Produkte</LocalizedLink>
90+
<LocalizedLink to="/products/">Produkte</LocalizedLink>
8891
```
8992

9093
If the configuration from above is used and the user views this link inside the i18n context `de-CH` the link will lead him to `/de/produkte`.
@@ -94,7 +97,7 @@ If the configuration from above is used and the user views this link inside the
9497
With the built-in `<GenericLocalizedLink>` component it's possible to use other plugins, which modify Gatsbys `<Link>` component. Here is an example with [Gatsby Plugin Transition Link](https://www.gatsbyjs.com/plugins/gatsby-plugin-transition-link/):
9598

9699
```jsx
97-
<GenericLocalizedLink to="/imprint">
100+
<GenericLocalizedLink to="/imprint/">
98101
{(args) => (
99102
<TransitionLink
100103
to={args.to}

src/components/GenericLocalizedLink.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('<LocalizedLink />', () => {
3131

3232
expect(component).toMatchInlineSnapshot(`
3333
<a
34-
href="/de/impressum"
34+
href="/de/impressum/"
3535
onClick={[Function]}
3636
onMouseEnter={[Function]}
3737
>
@@ -52,7 +52,7 @@ describe('<LocalizedLink />', () => {
5252

5353
expect(component).toMatchInlineSnapshot(`
5454
<a
55-
href="/de/contact"
55+
href="/de/contact/"
5656
onClick={[Function]}
5757
onMouseEnter={[Function]}
5858
>

src/components/GenericLocalizedLink.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { posix as nodePath } from 'path';
22
import { Fragment } from 'react';
33
import { useIntl } from 'react-intl';
4+
import { PluginOptions } from 'gatsby';
45
import { useI18nL10nContext } from '../contexts/I18nL10nContext';
5-
import { trimRightSlash } from '../utils/path';
6+
import { handleTrailingSlash } from '../utils/path';
67

78
type LinkProps = {
89
className?: string;
@@ -16,6 +17,7 @@ type LinkProps = {
1617

1718
type LocalizedLinkProps = {
1819
children: (args: LinkProps) => JSX.Element;
20+
trailingSlash?: PluginOptions['trailingSlash'];
1921
} & LinkProps;
2022

2123
export default function GenericLocalizedLink({
@@ -27,12 +29,14 @@ export default function GenericLocalizedLink({
2729
partiallyActive = true,
2830
replace,
2931
onClick,
32+
trailingSlash,
3033
}: LocalizedLinkProps) {
3134
const pageContext = useI18nL10nContext();
3235
const prefix = pageContext.prefix ?? '';
3336
const intl = useIntl();
3437
const getSlug = () => (intl.messages[to] ? intl.formatMessage({ id: to }) : to);
3538
const localizedPath = to !== '/' ? getSlug() : '/';
36-
const prefixedPath = intl.defaultLocale === intl.locale ? localizedPath : trimRightSlash(nodePath.join('/', prefix, localizedPath));
39+
const prefixedPath =
40+
intl.defaultLocale === intl.locale ? localizedPath : handleTrailingSlash(nodePath.join('/', prefix, localizedPath), trailingSlash);
3741
return <Fragment>{children({ to: prefixedPath, className, activeClassName, activeStyle, partiallyActive, replace, onClick })}</Fragment>;
3842
}

src/components/LocalizedLink.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ describe('<LocalizedLink />', () => {
3030

3131
expect(component).toMatchInlineSnapshot(`
3232
<a
33-
href="/de/impressum"
33+
href="/de/impressum/"
3434
onClick={[Function]}
3535
onMouseEnter={[Function]}
3636
>
@@ -51,7 +51,7 @@ describe('<LocalizedLink />', () => {
5151

5252
expect(component).toMatchInlineSnapshot(`
5353
<a
54-
href="/de/contact"
54+
href="/de/contact/"
5555
onClick={[Function]}
5656
onMouseEnter={[Function]}
5757
>

src/components/LocalizedLink.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { posix as nodePath } from 'path';
2-
import { Link } from 'gatsby';
2+
import { Link, PluginOptions } from 'gatsby';
33
import { PropsWithChildren } from 'react';
44
import { useIntl } from 'react-intl';
55
import { useI18nL10nContext } from '../contexts/I18nL10nContext';
6-
import { trimRightSlash } from '../utils/path';
6+
import { handleTrailingSlash } from '../utils/path';
77

88
type LocalizedLinkProps = PropsWithChildren<{
99
className?: string;
@@ -13,6 +13,7 @@ type LocalizedLinkProps = PropsWithChildren<{
1313
partiallyActive?: boolean;
1414
replace?: boolean;
1515
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
16+
trailingSlash?: PluginOptions['trailingSlash'];
1617
}>;
1718

1819
export default function LocalizedLink({
@@ -24,13 +25,15 @@ export default function LocalizedLink({
2425
partiallyActive = true,
2526
replace,
2627
onClick,
28+
trailingSlash,
2729
}: LocalizedLinkProps) {
2830
const pageContext = useI18nL10nContext();
2931
const prefix = pageContext.prefix ?? '';
3032
const intl = useIntl();
3133
const getSlug = () => (intl.messages[to] ? intl.formatMessage({ id: to }) : to);
3234
const localizedPath = to !== '/' ? getSlug() : '/';
33-
const prefixedPath = intl.defaultLocale === intl.locale ? localizedPath : trimRightSlash(nodePath.join('/', prefix, localizedPath));
35+
const prefixedPath =
36+
intl.defaultLocale === intl.locale ? localizedPath : handleTrailingSlash(nodePath.join('/', prefix, localizedPath), trailingSlash);
3437
return (
3538
<Link
3639
to={prefixedPath}

src/onCreateNode/translateNode.test.ts

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ const allNodes = [
127127
},
128128
fields: {
129129
translations: [
130-
{ locale: 'de-CH', path: '/old/translation' },
131-
{ locale: 'zh-CN', path: '/existing/translation' },
130+
{ locale: 'de-CH', path: '/old/translation/' },
131+
{ locale: 'zh-CN', path: '/existing/translation/' },
132132
],
133133
},
134134
},
@@ -172,14 +172,14 @@ describe('translateNode', () => {
172172
expect(createNodeField).toHaveBeenNthCalledWith(2, { node, name: 'filename', value: 'imprint' });
173173
expect(createNodeField).toHaveBeenNthCalledWith(3, { node, name: 'kind', value: 'pages' });
174174
expect(createNodeField).toHaveBeenNthCalledWith(4, { node, name: 'slug', value: 'impressum' });
175-
expect(createNodeField).toHaveBeenNthCalledWith(5, { node, name: 'path', value: '/de/pages/impressum' });
175+
expect(createNodeField).toHaveBeenNthCalledWith(5, { node, name: 'path', value: '/de/pages/impressum/' });
176176
expect(createNodeField).toHaveBeenNthCalledWith(6, { node, name: 'pathPrefix', value: 'de' });
177177
expect(createNodeField).toHaveBeenNthCalledWith(7, {
178178
node,
179179
name: 'translations',
180180
value: [
181-
{ locale: 'en-US', path: '/pages/imprint' },
182-
{ locale: 'fr-FR', path: '/fr/pages/imprimer' },
181+
{ locale: 'en-US', path: '/pages/imprint/' },
182+
{ locale: 'fr-FR', path: '/fr/pages/imprimer/' },
183183
],
184184
});
185185
});
@@ -196,14 +196,14 @@ describe('translateNode', () => {
196196
expect(createNodeField).toHaveBeenNthCalledWith(2, { node: currentNode, name: 'filename', value: 'imprint' });
197197
expect(createNodeField).toHaveBeenNthCalledWith(3, { node: currentNode, name: 'kind', value: 'pages' });
198198
expect(createNodeField).toHaveBeenNthCalledWith(4, { node: currentNode, name: 'slug', value: 'imprint' });
199-
expect(createNodeField).toHaveBeenNthCalledWith(5, { node: currentNode, name: 'path', value: '/pages/imprint' });
199+
expect(createNodeField).toHaveBeenNthCalledWith(5, { node: currentNode, name: 'path', value: '/pages/imprint/' });
200200
expect(createNodeField).toHaveBeenNthCalledWith(6, { node: currentNode, name: 'pathPrefix', value: '' });
201201
expect(createNodeField).toHaveBeenNthCalledWith(7, {
202202
node: currentNode,
203203
name: 'translations',
204204
value: [
205-
{ locale: 'de-CH', path: '/de/pages/impressum' },
206-
{ locale: 'fr-FR', path: '/fr/pages/imprimer' },
205+
{ locale: 'de-CH', path: '/de/pages/impressum/' },
206+
{ locale: 'fr-FR', path: '/fr/pages/imprimer/' },
207207
],
208208
});
209209
});
@@ -225,7 +225,7 @@ describe('translateNode', () => {
225225
expect(createNodeField).toHaveBeenNthCalledWith(2, { node: currentNode, name: 'filename', value: 'more' });
226226
expect(createNodeField).toHaveBeenNthCalledWith(3, { node: currentNode, name: 'kind', value: 'sections/special' });
227227
expect(createNodeField).toHaveBeenNthCalledWith(4, { node: currentNode, name: 'slug', value: 'more' });
228-
expect(createNodeField).toHaveBeenNthCalledWith(5, { node: currentNode, name: 'path', value: '/de/sections/special/more' });
228+
expect(createNodeField).toHaveBeenNthCalledWith(5, { node: currentNode, name: 'path', value: '/de/sections/special/more/' });
229229
expect(createNodeField).toHaveBeenNthCalledWith(6, { node: currentNode, name: 'pathPrefix', value: 'de' });
230230
expect(createNodeField).toHaveBeenNthCalledWith(7, {
231231
node: currentNode,
@@ -246,14 +246,14 @@ describe('translateNode', () => {
246246
expect(createNodeField).toHaveBeenNthCalledWith(2, { node, name: 'filename', value: 'imprint' });
247247
expect(createNodeField).toHaveBeenNthCalledWith(3, { node, name: 'kind', value: 'pages' });
248248
expect(createNodeField).toHaveBeenNthCalledWith(4, { node, name: 'slug', value: 'impressum' });
249-
expect(createNodeField).toHaveBeenNthCalledWith(5, { node, name: 'path', value: '/de/impressum' });
249+
expect(createNodeField).toHaveBeenNthCalledWith(5, { node, name: 'path', value: '/de/impressum/' });
250250
expect(createNodeField).toHaveBeenNthCalledWith(6, { node, name: 'pathPrefix', value: 'de' });
251251
expect(createNodeField).toHaveBeenNthCalledWith(7, {
252252
node,
253253
name: 'translations',
254254
value: [
255-
{ locale: 'en-US', path: '/imprint' },
256-
{ locale: 'fr-FR', path: '/fr/imprimer' },
255+
{ locale: 'en-US', path: '/imprint/' },
256+
{ locale: 'fr-FR', path: '/fr/imprimer/' },
257257
],
258258
});
259259
});
@@ -273,15 +273,15 @@ describe('translateNode', () => {
273273
locale: `de-CH`,
274274
prefix: `de`,
275275
slugs: {
276-
'/pages': '/seiten',
276+
'/pages/': '/seiten/',
277277
},
278278
messages: {},
279279
},
280280
{
281281
locale: `fr-FR`,
282282
prefix: `fr`,
283283
slugs: {
284-
'/pages': '/feuilles', // I know it's not a literal translation
284+
'/pages/': '/feuilles/', // I know it's not a literal translation
285285
},
286286
messages: {},
287287
},
@@ -295,29 +295,29 @@ describe('translateNode', () => {
295295
expect(createNodeField).toHaveBeenNthCalledWith(2, { node, name: 'filename', value: 'imprint' });
296296
expect(createNodeField).toHaveBeenNthCalledWith(3, { node, name: 'kind', value: 'pages' });
297297
expect(createNodeField).toHaveBeenNthCalledWith(4, { node, name: 'slug', value: 'impressum' });
298-
expect(createNodeField).toHaveBeenNthCalledWith(5, { node, name: 'path', value: '/de/seiten/impressum' });
298+
expect(createNodeField).toHaveBeenNthCalledWith(5, { node, name: 'path', value: '/de/seiten/impressum/' });
299299
expect(createNodeField).toHaveBeenNthCalledWith(6, { node, name: 'pathPrefix', value: 'de' });
300300
expect(createNodeField).toHaveBeenNthCalledWith(7, {
301301
node,
302302
name: 'translations',
303303
value: [
304-
{ locale: 'en-US', path: '/pages/imprint' },
305-
{ locale: 'fr-FR', path: '/fr/feuilles/imprimer' },
304+
{ locale: 'en-US', path: '/pages/imprint/' },
305+
{ locale: 'fr-FR', path: '/fr/feuilles/imprimer/' },
306306
],
307307
});
308308

309309
expect(createNodeField).toHaveBeenNthCalledWith(8, {
310310
node: allNodes.find((n) => n.id === 'm2'),
311311
name: 'translations',
312312
value: [
313-
{ locale: 'de-CH', path: '/de/seiten/impressum' },
314-
{ locale: 'zh-CN', path: '/existing/translation' },
313+
{ locale: 'de-CH', path: '/de/seiten/impressum/' },
314+
{ locale: 'zh-CN', path: '/existing/translation/' },
315315
],
316316
});
317317
expect(createNodeField).toHaveBeenNthCalledWith(9, {
318318
node: allNodes.find((n) => n.id === 'm1'),
319319
name: 'translations',
320-
value: [{ locale: 'de-CH', path: '/de/seiten/impressum' }],
320+
value: [{ locale: 'de-CH', path: '/de/seiten/impressum/' }],
321321
});
322322
});
323323

@@ -336,15 +336,15 @@ describe('translateNode', () => {
336336
locale: `de-CH`,
337337
prefix: `de`,
338338
slugs: {
339-
'/pages': '/seiten',
339+
'/pages/': '/seiten/',
340340
},
341341
messages: {},
342342
},
343343
{
344344
locale: `fr-FR`,
345345
prefix: `fr`,
346346
slugs: {
347-
'/pages': '/feuilles', // I know it's not a literal translation
347+
'/pages/': '/feuilles/', // I know it's not a literal translation
348348
},
349349
messages: {},
350350
},

src/onCreateNode/translateNode.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { FileSystemNode } from 'gatsby-source-filesystem';
33
import convertToSlug from 'limax';
44
import { Actions, CreateNodeArgs, Node, NodePluginArgs, PluginOptions } from 'gatsby';
55
import { Frontmatter, Translation } from '../../types';
6-
import { addLocalePrefix, replaceSegmentsWithSlugs, trimRightSlash } from '../utils/path';
6+
import { addLocalePrefix, handleTrailingSlash, replaceSegmentsWithSlugs } from '../utils/path';
77
import { findClosestLocale, parseFilenameSuffix } from '../utils/i18n';
88

99
const MARKDOWN_TYPES = ['MarkdownRemark', 'Mdx'];
@@ -140,6 +140,9 @@ const translatePath = (filename: string, relativeDirectory: string, locale: stri
140140
filepath = replaceSegmentsWithSlugs(filepath, localeOption.slugs);
141141
}
142142

143+
// handle trailing slash
144+
filepath = handleTrailingSlash(filepath, options.trailingSlash);
145+
143146
return { slug, kind: relativeDirectory, filepath };
144147
};
145148

@@ -169,7 +172,7 @@ export const translateNode = async ({ getNode, getNodes, node, actions }: Create
169172
const siblings = findTranslations(getNodes(), absolutePath, options);
170173
const translations = getAvailableTranslations(siblings, getNode, options).map((t) => {
171174
const { filepath: translatedFilepath } = translatePath(t.filename, relativeDirectory, t.locale, options, t.title);
172-
return { path: trimRightSlash(translatedFilepath), locale: t.locale };
175+
return { path: handleTrailingSlash(translatedFilepath, options.trailingSlash), locale: t.locale };
173176
});
174177

175178
createNodeField({ node, name: 'locale', value: locale });

src/onCreatePage/__snapshots__/translatePage.test.ts.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ exports[`translatePage should obey page blacklists 1`] = `
1313
"translations": [
1414
{
1515
"locale": "zh-CN",
16-
"path": "/zh/do-not-translate-to-german",
16+
"path": "/zh/do-not-translate-to-german/",
1717
},
1818
],
1919
},
2020
"isCreatedByStatefulCreatePages": true,
21-
"path": "/do-not-translate-to-german",
21+
"path": "/do-not-translate-to-german/",
2222
},
2323
],
2424
[
@@ -31,12 +31,12 @@ exports[`translatePage should obey page blacklists 1`] = `
3131
"translations": [
3232
{
3333
"locale": "en-US",
34-
"path": "/do-not-translate-to-german",
34+
"path": "/do-not-translate-to-german/",
3535
},
3636
],
3737
},
3838
"isCreatedByStatefulCreatePages": true,
39-
"path": "/zh/do-not-translate-to-german",
39+
"path": "/zh/do-not-translate-to-german/",
4040
},
4141
],
4242
],

0 commit comments

Comments
 (0)