Skip to content

Commit e0ede30

Browse files
committed
feat: add better localization support
1 parent a6456a9 commit e0ede30

File tree

9 files changed

+427
-259
lines changed

9 files changed

+427
-259
lines changed

index.js

Lines changed: 1 addition & 237 deletions
Original file line numberDiff line numberDiff line change
@@ -1,237 +1 @@
1-
import fs from 'node:fs/promises';
2-
import Handlebars from 'handlebars';
3-
import { minify } from 'html-minifier';
4-
import { marked } from 'marked';
5-
6-
const DATE_LOCALE = 'en-US';
7-
/** @type {Intl.DateTimeFormatOptions} */
8-
const LONG_DATE_FORMAT = { month: 'short', year: 'numeric' };
9-
/** @type {Intl.DateTimeFormatOptions} */
10-
const SHORT_DATE_FORMAT = { year: 'numeric' };
11-
12-
/**
13-
* Custom renderer for marked, namely to disable unwanted features. We only want
14-
* to allow basic inline elements, like links, bold, or inline-code.
15-
*
16-
* @type {import('marked').RendererObject}
17-
*/
18-
const renderer = {
19-
heading(heading) {
20-
return heading.text;
21-
},
22-
html(html) {
23-
return html.text;
24-
},
25-
hr() {
26-
return '';
27-
},
28-
list(list) {
29-
return list.raw;
30-
},
31-
listitem(text) {
32-
return text.text;
33-
},
34-
br() {
35-
return '';
36-
},
37-
paragraph(text) {
38-
return text.text;
39-
}
40-
}
41-
42-
marked.use({ renderer });
43-
44-
/**
45-
* Plugins to enable to minify HTML after generating from the template.
46-
*/
47-
const minifyOptions = {
48-
collapseBooleanAttributes: true,
49-
collapseWhitespace: true,
50-
decodeEntities: true,
51-
minifyCSS: true,
52-
removeComments: true,
53-
removeRedundantAttributes: true,
54-
sortAttributes: true,
55-
sortClassName: true,
56-
};
57-
58-
Handlebars.registerHelper('date', /** @param {string} body */ (body) => {
59-
if (!body) {
60-
return 'Present'
61-
}
62-
63-
const date = new Date(body);
64-
const datetime = date.toISOString();
65-
const localeString = body.split('-').length !== 1
66-
? date.toLocaleDateString(DATE_LOCALE, LONG_DATE_FORMAT)
67-
: date.toLocaleDateString(DATE_LOCALE, SHORT_DATE_FORMAT);
68-
69-
return `<time datetime="${datetime}">${localeString}</time>`;
70-
});
71-
72-
Handlebars.registerHelper('markdown', /** @param {string} body */ (body) => {
73-
return marked.parse(body);
74-
});
75-
76-
Handlebars.registerHelper('link', /** @param {string} body */ (body) => {
77-
const parsed = new URL(body);
78-
const host = (parsed.host.startsWith('www.')) ? parsed.host.substring(4) : parsed.host;
79-
return `<a href="${body}">${host}</a>`;
80-
});
81-
82-
/**
83-
* Many users still have JSON Resumes written against old versions of the
84-
* schema. We detect this and upgrade them to the latest version behind the
85-
* scenes.
86-
*
87-
* Writes to the object directly.
88-
*
89-
* @param {any} resume
90-
* @returns {boolean}
91-
* If the JSON Resume was modified. (i.e. was using outdated property names)
92-
*
93-
* @see https://github.com/jsonresume/resume-schema/releases/tag/v0.0.17
94-
* @see https://github.com/jsonresume/resume-schema/releases/tag/v0.0.12
95-
*/
96-
function upgradeOutdatedResume(resume) {
97-
let upgraded = false;
98-
99-
if (resume.bio && !resume.basics) {
100-
resume.basics = resume.bio;
101-
upgraded = true;
102-
}
103-
104-
if ((resume.basics?.firstName || resume.basics?.lastName) && !resume.basics.name) {
105-
const names = [];
106-
107-
if (resume.basics.firstName) {
108-
names.push(resume.basics.firstName);
109-
}
110-
111-
if (resume.basics.lastName) {
112-
names.push(resume.basics.lastName);
113-
}
114-
115-
resume.basics.name = names.join(' ');
116-
upgraded = true;
117-
}
118-
119-
if (resume.basics?.picture && !resume.basics.image) {
120-
resume.basics.image = resume.basics.picture;
121-
upgraded = true;
122-
}
123-
124-
if (resume.basics?.website && !resume.basics.url) {
125-
resume.basics.url = resume.basics.website;
126-
upgraded = true;
127-
}
128-
129-
if (resume.basics?.state && !resume.basics?.region) {
130-
resume.basics.region = resume.basics.state;
131-
upgraded = true;
132-
}
133-
134-
if (Array.isArray(resume.work)) {
135-
for (const work of resume.work) {
136-
if (work?.company && !work.name) {
137-
work.name = work.company;
138-
upgraded = true;
139-
}
140-
141-
if (work?.website && !work.url) {
142-
work.url = work.website;
143-
upgraded = true;
144-
}
145-
}
146-
}
147-
148-
if (Array.isArray(resume.volunteer)) {
149-
for (const volunteer of resume.volunteer) {
150-
if (volunteer?.website && !volunteer.url) {
151-
volunteer.url = volunteer.website;
152-
upgraded = true;
153-
}
154-
}
155-
}
156-
157-
if (Array.isArray(resume.publications)) {
158-
for (const publication of resume.publications) {
159-
if (publication?.website && !publication.url) {
160-
publication.url = publication.website;
161-
upgraded = true;
162-
}
163-
}
164-
}
165-
166-
if (resume.hobbies && !resume.interests) {
167-
resume.interests = resume.hobbies;
168-
upgraded = true;
169-
}
170-
171-
if (Array.isArray(resume.languages)) {
172-
for (const language of resume.languages) {
173-
if (language?.name && !language.language) {
174-
language.language = language.name;
175-
upgraded = true;
176-
}
177-
178-
if (language?.level && !language.fluency) {
179-
language.fluency = language.level;
180-
upgraded = true;
181-
}
182-
}
183-
}
184-
185-
return upgraded;
186-
}
187-
188-
/**
189-
* @param {any} resume
190-
* @returns {Promise<string>}
191-
*/
192-
export async function render(resume) {
193-
const loading = Promise.all([
194-
fs.readFile(import.meta.dirname + '/style.css', 'utf-8'),
195-
fs.readFile(import.meta.dirname + '/resume.handlebars', 'utf-8'),
196-
]);
197-
198-
if (upgradeOutdatedResume(resume)) {
199-
console.warn('⚠️ Resume is written against an outdated version of the JSON Resume schema.\n⚠️ This will still work, but you should consider updating your resume.\n⚠️ See: https://jsonresume.org/schema');
200-
}
201-
202-
if (Array.isArray(resume.basics?.profiles)) {
203-
const { profiles } = resume.basics;
204-
const xTwitter = profiles.find((profile) => {
205-
const name = profile.network.toLowerCase();
206-
return name === 'x' || name === 'twitter';
207-
});
208-
209-
if (xTwitter) {
210-
let { username, url } = xTwitter;
211-
212-
if (!username && url) {
213-
const match = url.match(/https?:\/\/.+?\/(\w{1,15})/);
214-
215-
if (match.length == 2) {
216-
username = match[1];
217-
}
218-
}
219-
220-
if (username && !username.startsWith('@')) {
221-
username = `@${username}`;
222-
}
223-
224-
resume.custom = {
225-
xTwitterHandle: username
226-
}
227-
}
228-
}
229-
230-
const [css, template] = await loading;
231-
const html = Handlebars.compile(template)({
232-
css,
233-
resume
234-
});
235-
236-
return minify(html, minifyOptions);
237-
}
1+
export { render } from './src/index.js';

package.json

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,11 @@
1919
"url": "https://github.com/jsonresume/jsonresume-theme-class"
2020
},
2121
"files": [
22-
"LICENSE",
23-
"README.md",
2422
"index.js",
23+
"LICENSE",
2524
"package.json",
26-
"resume.handlebars",
27-
"style.css"
25+
"README.md",
26+
"src/"
2827
],
2928
"scripts": {
3029
"serve": "resume serve --theme . --resume ./test/fixture.resume.json",
@@ -39,6 +38,8 @@
3938
"node": ">=v20.11"
4039
},
4140
"dependencies": {
41+
"@fluent/bundle": "^0.19.1",
42+
"@fluent/langneg": "^0.7.0",
4243
"handlebars": "^4.7.8",
4344
"html-minifier": "^4.0.0",
4445
"marked": "^16.4.1"

scripts/generate-preview.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'node:path';
22
import puppeteer from 'puppeteer';
3-
import { render } from '../index.js';
3+
import { render } from '../src/index.js';
44
import resume from '../test/fixture.resume.json' with { type: 'json' };
55

66
const html = await render(resume);

src/i18n/locales/en.ftl

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
title =
2+
{$name} - CV
3+
4+
avatar-description =
5+
Avatar of {$name}.
6+
7+
curriculum-vitae =
8+
CV
9+
10+
contact =
11+
Contact
12+
13+
website =
14+
Website
15+
16+
email =
17+
Email
18+
19+
phone =
20+
Phone
21+
22+
about =
23+
About
24+
25+
work-experience =
26+
Work Experience
27+
28+
volunteer =
29+
Volunteer
30+
31+
projects =
32+
Projects
33+
34+
education =
35+
Education
36+
37+
dissertation =
38+
Dissertation: {$text}
39+
40+
skills =
41+
Skills
42+
43+
languages =
44+
Languages
45+
46+
interests =
47+
Interests
48+
49+
references =
50+
References
51+
52+
present =
53+
Present

0 commit comments

Comments
 (0)