Skip to content

Commit 789b427

Browse files
first article done and folder structure refactored
1 parent 0ab3fa7 commit 789b427

File tree

18 files changed

+459
-517
lines changed

18 files changed

+459
-517
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
"next": "^10.0.0",
2424
"react": "17.0.1",
2525
"react-dom": "17.0.1",
26-
"remark": "^12.0.0",
27-
"remark-html": "^12.0.0",
26+
"react-markdown": "^6.0.2",
27+
"react-syntax-highlighter": "^15.4.3",
2828
"styled-components": "^5.3.0",
2929
"tailwindcss": "^2.1.2",
3030
"twin.macro": "^2.4.2"
@@ -34,6 +34,7 @@
3434
"@types/jest": "^26.0.23",
3535
"@types/node": "^15.6.0",
3636
"@types/react": "^17.0.6",
37+
"@types/react-syntax-highlighter": "^13.5.0",
3738
"@types/react-test-renderer": "^17.0.1",
3839
"@types/styled-components": "^5.1.9",
3940
"@typescript-eslint/eslint-plugin": "^4.24.0",

src/components/layout.module.css

Lines changed: 0 additions & 15 deletions
This file was deleted.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
---
2+
title: 'Accessing i18next translation from JSON keys instead of string path'
3+
date: '2021-05-25'
4+
---
5+
6+
**TypeScript** is awesome. Like **i18next** too. It works with TypeScript great but has one drawback. We need to provide a translation key as a plain string. It doesn't matter for small apps, but it does indeed for complex one when your translation file start to have hundreds of lines.
7+
8+
In this article, I want to show the way to solve that. It will leverage our localization experience to the next level.
9+
10+
## Goal
11+
12+
When using TypeScript we expect that tools will behave in some beneficial way like:
13+
14+
- we can use IDE **IntelliSense** to pick a specific key from object structure instead of the path as a string,
15+
- we can **easily refactor** translations structure and still trust our code,
16+
- we want to be **able to navigate** to a specific key location in the translation object structure,
17+
18+
So let's look at the solution.
19+
20+
What i18next offers as an out of the box looks like this:
21+
22+
```ts
23+
// translation.ts
24+
25+
import en from './en.json';
26+
import i18next from 'i18next';
27+
28+
i18next.init({
29+
lng: 'en',
30+
resources: {
31+
en: {
32+
translation: en,
33+
},
34+
},
35+
});
36+
```
37+
38+
When having translation file like:
39+
40+
```json
41+
{
42+
// en.json
43+
44+
"homePage": {
45+
"title": "Home page",
46+
"homaPage": {
47+
"header": {
48+
"buttons": {
49+
"signIn": { "title": "Sign In" },
50+
"signUp": { "title": "Sign Up" }
51+
}
52+
}
53+
}
54+
}
55+
}
56+
```
57+
58+
We can access specific translated value by typing key as a string path:
59+
60+
```ts
61+
i18next.t('homePage.header.buttons.signIn.title');
62+
```
63+
64+
As you can see, by having the **translation key as a string**, we are losing all benefits described above.
65+
66+
What we want to achieve is something that looks like this:
67+
68+
```ts
69+
i18next.t(keys.homePage.header.buttons.signIn.title);
70+
```
71+
72+
What fun is, typescript allows us already to load JSON files into code and take benefits from taking IntelliSense working already there.
73+
74+
Now we need a new object named keys with the same structure as our JSON file. But instead of translated values at every leaf, we need to have **that key as a path**. Because i18next.t function still needs that path.
75+
76+
So by accessing:
77+
78+
```ts
79+
keys.homePage.header.buttons.signIn.title;
80+
```
81+
82+
We want to recievie:
83+
84+
```ts
85+
'homePage.header.buttons.signIn.title';
86+
```
87+
88+
To achieve this, we need to transform the JSON file into another one like this:
89+
90+
```ts
91+
import { reduce } from 'lodash';
92+
93+
const getTranslationKeys = <T>(translations: T, path = ''): T =>
94+
reduce(
95+
translations,
96+
(accumulator, value, key) => {
97+
const newPath = `${path}${!!path ? '.' : ''}${key}`;
98+
return {
99+
...accumulator,
100+
[key]: isObject(value) ? getTranslationKeys(value, newPath) : newPath,
101+
};
102+
},
103+
{}
104+
);
105+
```
106+
107+
This code will **recursively** enumerate all the key/value pairs. When a value is an object it will call himself with path collected so far. Otherwise, it reaches the leaf and returns the path. Finally, all the leaves will contains paths as expected.
108+
109+
Now we can create keys object and get our translation:
110+
111+
```ts
112+
import en from './en.json';
113+
import i18next from 'i18next';
114+
115+
i18next.init({
116+
lng: 'en',
117+
resources: {
118+
en: {
119+
translation: en,
120+
},
121+
},
122+
});
123+
124+
const translationKeys = getTranslationKeys(en);
125+
126+
const value = i18next.t(translationKeys.homePage.header.buttons.signIn.title);
127+
```
128+
129+
Finally, we've reached the goal.
130+
Now the TypeScript and IDE tools are active when we reference the translation keys. And we can trust our code more.
131+
132+
See also [i18n-keys](https://github.com/WojciechCendrzak/i18n-keys) library that fully implements this concept.
133+
134+
Thanks for reading.

src/logic/posts.ts

Lines changed: 0 additions & 71 deletions
This file was deleted.

src/models/post.ts

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`HomePage should match the snapshot 1`] = `
4+
<div
5+
className="sc-bdnxRM DVYBs"
6+
>
7+
<header>
8+
<h1>
9+
[Your Name]
10+
11+
</h1>
12+
</header>
13+
<main>
14+
<section>
15+
<p>
16+
About me
17+
</p>
18+
<p>
19+
I'am frontend developer
20+
</p>
21+
</section>
22+
<section>
23+
<h2>
24+
Blog
25+
</h2>
26+
<ul />
27+
</section>
28+
</main>
29+
</div>
30+
`;

src/pages/home.page.test.tsx renamed to src/pages-internal/home/home.page.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import HomePage from './home.page';
33

44
describe('HomePage', () => {
55
it('should match the snapshot', () => {
6-
const tree = renderer.create(<HomePage allPostsData={[]} />).toJSON();
6+
const tree = renderer.create(<HomePage postDescriptions={[]} />).toJSON();
77
expect(tree).toMatchSnapshot();
88
});
99
});
Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import Head from 'next/head';
2-
import { Layout } from '../components/layout';
3-
import { getSortedPostsData } from '../logic/posts';
2+
import { Layout } from '../../components/layout';
3+
import { getPostDescriptions } from '../post/post.logic';
44
import Link from 'next/link';
5-
import { Date } from '../components/date';
5+
import { Date } from '../../components/date';
66
import { GetStaticProps } from 'next';
77
import React from 'react';
8-
import { Post } from '../models/post';
9-
import { translate, translationKeys } from '../logic/translations/translation.service';
8+
import { PostDescription } from '../post/post.model';
9+
import { translate, translationKeys } from '../../logic/translations/translation.service';
1010

1111
interface HomeProps {
12-
allPostsData: Post[];
12+
postDescriptions: PostDescription[];
1313
}
1414

15-
export const HomePage: React.FC<HomeProps> = ({ allPostsData }) => {
15+
export const HomePage: React.FC<HomeProps> = ({ postDescriptions }) => {
1616
return (
1717
<Layout home>
1818
<Head>
@@ -25,9 +25,9 @@ export const HomePage: React.FC<HomeProps> = ({ allPostsData }) => {
2525
<section>
2626
<h2>Blog</h2>
2727
<ul>
28-
{allPostsData.map(({ id, date, title }) => (
28+
{postDescriptions.map(({ id, date, title }) => (
2929
<li key={id}>
30-
<Link href={`/posts/${id}`}>
30+
<Link href={`/post/${id}`}>
3131
<a>{title}</a>
3232
</Link>
3333
<br />
@@ -45,10 +45,10 @@ export const HomePage: React.FC<HomeProps> = ({ allPostsData }) => {
4545
};
4646

4747
export const getStaticProps: GetStaticProps<HomeProps> = async () => {
48-
const allPostsData = getSortedPostsData();
48+
const postDescriptions = getPostDescriptions();
4949
return {
5050
props: {
51-
allPostsData,
51+
postDescriptions,
5252
},
5353
};
5454
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import fs from 'fs';
2+
import path from 'path';
3+
import matter from 'gray-matter';
4+
import { Post, PostDescription, PostMeta } from './post.model';
5+
6+
const postsDirectory = path.join(process.cwd(), 'src/content');
7+
8+
export const getPostDescriptions = (): PostDescription[] => {
9+
const fileNames = fs.readdirSync(postsDirectory);
10+
const postsMeta = fileNames.map((fileName) => {
11+
const id = fileName.replace(/\.md$/, '');
12+
const fullPath = path.join(postsDirectory, fileName);
13+
const fileContents = fs.readFileSync(fullPath, 'utf8');
14+
const matterResult = matter(fileContents);
15+
16+
return {
17+
id,
18+
...(matterResult.data as PostMeta),
19+
};
20+
});
21+
22+
return postsMeta.sort((a, b) => {
23+
if (!a.date || !b.date) return 0;
24+
25+
if (a.date < b.date) {
26+
return 1;
27+
} else {
28+
return -1;
29+
}
30+
});
31+
};
32+
33+
export const getPostIds = () => {
34+
const fileNames = fs.readdirSync(postsDirectory);
35+
return fileNames.map((fileName) => {
36+
return {
37+
params: {
38+
id: fileName.replace(/\.md$/, ''),
39+
},
40+
};
41+
});
42+
};
43+
44+
export const getPostData = async (id: string): Promise<Post> => {
45+
const fullPath = path.join(postsDirectory, `${id}.md`);
46+
const fileContent = fs.readFileSync(fullPath, 'utf8');
47+
const postMatter = matter(fileContent);
48+
49+
return {
50+
id,
51+
content: postMatter.content,
52+
...(postMatter.data as PostMeta),
53+
};
54+
};

0 commit comments

Comments
 (0)