Compare commits

...

3 Commits
master ... i18n

Author SHA1 Message Date
Reorx b379cd6be5 complete en locale for reorx theme 2023-12-08 21:08:50 +08:00
Reorx 911df68cd1 add meta.locale in jsoncv schema, use it when rendering the theme 2023-12-08 21:00:55 +08:00
Reorx 44808bf31e complete i18n PoC 2023-12-08 20:46:15 +08:00
12 changed files with 1534 additions and 27 deletions

View File

@ -250,6 +250,7 @@ See [Does JavaScript guarantee object property order? - Stack Overflow](https://
- [x] Allows customizing primary color for the current theme - [x] Allows customizing primary color for the current theme
- [x] Export PDF directly (using browser's print feature) - [x] Export PDF directly (using browser's print feature)
- [x] Supports responsive style for themes, so that the CV site is friendly to view on mobile devices. - [x] Supports responsive style for themes, so that the CV site is friendly to view on mobile devices.
- [ ] i18n and language switcher
- [ ] Add more themes. - [ ] Add more themes.
- [ ] Allows switching themes in Editor - [ ] Allows switching themes in Editor
- [ ] Add more sample data. By clicking the "Load Sample" button, a dialog will open, allowing the user to select from various languages - [ ] Add more sample data. By clicking the "Load Sample" button, a dialog will open, allowing the user to select from various languages

1459
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,7 @@
"ejs": "^3.1.8", "ejs": "^3.1.8",
"iconify-icon": "^1.0.3", "iconify-icon": "^1.0.3",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"node-polyglot": "^2.5.0",
"object-path": "^0.11.8" "object-path": "^0.11.8"
} }
} }

View File

@ -280,6 +280,8 @@
} }
], ],
"meta": { "meta": {
"name": "JSONCV Sample Resume",
"locale": "en",
"canonical": "https://raw.githubusercontent.com/reorx/jsoncv/master/schema/jsoncv.schema.json", "canonical": "https://raw.githubusercontent.com/reorx/jsoncv/master/schema/jsoncv.schema.json",
"version": "v2.0.0", "version": "v2.0.0",
"lastModified": "2023-02-12T22:26:00" "lastModified": "2023-02-12T22:26:00"

View File

@ -509,6 +509,10 @@
"type": "string", "type": "string",
"description": "The name of this JSONCV data, will be used as part of the filename when downloading." "description": "The name of this JSONCV data, will be used as part of the filename when downloading."
}, },
"locale": {
"type": "string",
"description": "Locale of the CV, used by the theme to localize the CV. The value must be a valid IETF language tag. Default to 'en' if not specified. A full list of locale tag can be found at: https://cdn.jsdelivr.net/npm/dayjs@1/locale.json"
},
"canonical": { "canonical": {
"type": "string", "type": "string",
"description": "URL (as per RFC 3986) to latest version of this document", "description": "URL (as per RFC 3986) to latest version of this document",

View File

@ -25,7 +25,7 @@ import { getCVTitle } from '../themes/data';
import { registerIconLib } from './je-iconlib'; import { registerIconLib } from './je-iconlib';
import { registerTheme } from './je-theme'; import { registerTheme } from './je-theme';
const propertiesInOrder = ['basics', 'education', 'work', 'projects', 'sideProjects', 'skills', 'languages', 'interests', 'references', 'awards', 'publications', 'volunteer', 'certificates', 'meta'] const propertiesInOrder = ['meta', 'basics', 'education', 'work', 'projects', 'sideProjects', 'skills', 'languages', 'interests', 'references', 'awards', 'publications', 'volunteer', 'certificates']
const basicsPropertiesInOrder = ['name', 'label', 'email', 'phone', 'url', 'summary', 'image', 'location', 'profiles'] const basicsPropertiesInOrder = ['name', 'label', 'email', 'phone', 'url', 'summary', 'image', 'location', 'profiles']
// toc elements // toc elements

View File

@ -4,9 +4,11 @@ import { renderMarkdown } from '../lib/markdown';
export const varNamePrimaryColor = '--color-primary' export const varNamePrimaryColor = '--color-primary'
export function getRenderData(cvData) { export function getRenderData(cvData, locale, polyglot) {
return { return {
cv: cvData, cv: cvData,
locale,
t: polyglot.t.bind(polyglot),
fn: { fn: {
getCVTitle, getCVTitle,
reformatDate, reformatDate,

View File

@ -1,4 +1,6 @@
import dayjs from 'dayjs';
import ejs from 'ejs'; import ejs from 'ejs';
import Polyglot from 'node-polyglot';
import { upsertStyleTag } from '../lib/utils'; import { upsertStyleTag } from '../lib/utils';
import { import {
@ -15,13 +17,17 @@ const themeNames = ['reorx']
// cannot be used because we need vite to transform scss into css // cannot be used because we need vite to transform scss into css
const styleMoudules = import.meta.glob("./*/index.scss", { "query": "?inline" }) const styleMoudules = import.meta.glob("./*/index.scss", { "query": "?inline" })
console.log('themes index')
for (const name of themeNames) { for (const name of themeNames) {
const templateModule = await import(`./${name}/index.ejs`) const templateModule = await import(`./${name}/index.ejs`)
const themeModule = await import(`./${name}/index.js`)
console.log(`theme supported locales: ${themeModule.locales}`)
// https://vitejs.dev/guide/features.html#glob-import // https://vitejs.dev/guide/features.html#glob-import
const styleModule = await styleMoudules[`./${name}/index.scss`]() const styleModule = await styleMoudules[`./${name}/index.scss`]()
themes[name] = { themes[name] = {
index: themeModule,
template: templateModule.default, template: templateModule.default,
style: styleModule.default, style: styleModule.default,
} }
@ -34,17 +40,31 @@ export function getTheme(name) {
return themes[name] return themes[name]
} }
export function renderTheme(template, cvData, options) { export function renderTheme(theme, cvData, options) {
return ejs.render(template, getRenderData(cvData), options) const locale = cvData.meta.locale || 'en'
const messages = theme.index.localeMessages[locale]
if (!messages) {
return `Error: locale '${locale}' is not supported, please use one of: ${theme.index.locales}`
}
const polyglot = new Polyglot({
phrases: messages,
locale,
})
dayjs.locale(locale)
return ejs.render(theme.template, getRenderData(cvData, locale, polyglot), options)
} }
const cvStyleId = 'cv-style' const cvStyleId = 'cv-style'
export function renderThemeOn(name, el, data, primaryColor) { export function renderThemeOn(name, el, data, primaryColor) {
const theme = getTheme(name) const theme = getTheme(name)
el.innerHTML = renderTheme(theme.template, data) el.innerHTML = renderTheme(theme, data)
upsertStyleTag(cvStyleId, theme.style) upsertStyleTag(cvStyleId, theme.style)
document.documentElement.style.setProperty(varNamePrimaryColor, primaryColor) document.documentElement.style.setProperty(varNamePrimaryColor, primaryColor)
} }
export function loadThemeData(name) {
require(`./${name}/index.js`)
}

View File

@ -64,10 +64,10 @@ function dateRange(item, level) {
// level: 1: year, 2: month, 3: day // level: 1: year, 2: month, 3: day
switch (level) { switch (level) {
case 1: case 1:
format = 'YYYY' format = t('format.year')
break; break;
case 2: case 2:
format = 'MMM YYYY' format = t('format.year_month')
break; break;
} }
if (format) { if (format) {
@ -90,7 +90,7 @@ function dateRange(item, level) {
<% if (hasItems(cv.education)) { %> <% if (hasItems(cv.education)) { %>
<section class="education-section"> <section class="education-section">
<div class="section-title"> <div class="section-title">
<h2>Educations</h2> <h2><%= t('title.education') %></h2>
<div class="line"></div> <div class="line"></div>
</div> </div>
<% for (const item of cv.education) { %> <% for (const item of cv.education) { %>
@ -105,10 +105,10 @@ function dateRange(item, level) {
</div> </div>
<%- dateRange(item, 2) %> <%- dateRange(item, 2) %>
</div> </div>
<% if (item.score) { %><div class="score row">Overall GPA: <%= item.score %></div><% } %> <% if (item.score) { %><div class="score row"><%= t('overall_gpa') %>: <%= item.score %></div><% } %>
<% if (item.courses && item.courses.length > 0) { %> <% if (item.courses && item.courses.length > 0) { %>
<div class="courses row"> <div class="courses row">
Courses: <%= item.courses.join('; ') %> <%= t('courses') %>: <%= item.courses.join('; ') %>
</div> </div>
<% } %> <% } %>
</div> </div>
@ -120,7 +120,7 @@ function dateRange(item, level) {
<% if (hasItems(cv.work)) { %> <% if (hasItems(cv.work)) { %>
<section class="work-section"> <section class="work-section">
<div class="section-title"> <div class="section-title">
<h2>Work</h2> <h2><%= t('title.work') %></h2>
<div class="line"></div> <div class="line"></div>
</div> </div>
<% for (const item of cv.work) { %> <% for (const item of cv.work) { %>
@ -152,7 +152,7 @@ function dateRange(item, level) {
<% if (hasItems(cv.projects)) { %> <% if (hasItems(cv.projects)) { %>
<section class="projects-section"> <section class="projects-section">
<div class="section-title"> <div class="section-title">
<h2>Projects</h2> <h2><%= t('title.projects') %></h2>
<div class="line"></div> <div class="line"></div>
</div> </div>
<% for (const item of cv.projects) { %> <% for (const item of cv.projects) { %>
@ -195,7 +195,7 @@ function dateRange(item, level) {
<% if (hasItems(cv.sideProjects)) { %> <% if (hasItems(cv.sideProjects)) { %>
<section class="sideprojects-section"> <section class="sideprojects-section">
<div class="section-title"> <div class="section-title">
<h2>Side-projects</h2> <h2><%= t('title.side_projects') %></h2>
<div class="line"></div> <div class="line"></div>
</div> </div>
<div class="two-columns"> <div class="two-columns">
@ -228,7 +228,7 @@ function dateRange(item, level) {
<% if (hasItems(cv.skills)) { %> <% if (hasItems(cv.skills)) { %>
<section class="skills-section"> <section class="skills-section">
<div class="section-title"> <div class="section-title">
<h2>Skills</h2> <h2><%= t('title.skills') %></h2>
<div class="line"></div> <div class="line"></div>
</div> </div>
<div class="two-columns"> <div class="two-columns">
@ -256,7 +256,7 @@ function dateRange(item, level) {
<% if (hasItems(cv.languages)) { %> <% if (hasItems(cv.languages)) { %>
<section class="languages-section page-unit"> <section class="languages-section page-unit">
<div class="section-title"> <div class="section-title">
<h2>Languages</h2> <h2><%= t('title.languages') %></h2>
<div class="line"></div> <div class="line"></div>
</div> </div>
<% for (const item of cv.languages) { %> <% for (const item of cv.languages) { %>
@ -283,10 +283,10 @@ function dateRange(item, level) {
<% if (cv.meta) { %> <% if (cv.meta) { %>
<footer> <footer>
<% if (cv.meta.version) { %> <% if (cv.meta.version) { %>
<div class="version">Version: <%= cv.meta.version %></div> <div class="version"><%= t('version') %>: <%= cv.meta.version %></div>
<% } %> <% } %>
<% if (cv.meta.lastModified) { %> <% if (cv.meta.lastModified) { %>
<div class="version">Last modified: <%= cv.meta.lastModified.slice(0, 10) %></div> <div class="version"><%= t('last_modified') %>: <%= cv.meta.lastModified.slice(0, 10) %></div>
<% } %> <% } %>
</footer> </footer>
<% } %> <% } %>

11
src/themes/reorx/index.js Normal file
View File

@ -0,0 +1,11 @@
import 'dayjs/locale/zh-cn';
export const locales = ['en', 'zh-cn']
export const localeMessages = {}
for (const locale of locales) {
localeMessages[locale] = (await import(`./locales/${locale}.js`)).default
}
console.log('load theme reorx', localeMessages)

View File

@ -0,0 +1,18 @@
export default {
'title': {
'education': 'Educations',
'work': 'Work Experiences',
'projects': 'Projects',
'side_projects': 'Side-projects',
'skills': 'Skills',
'languages': 'Languages',
},
'overall_gpa': 'Overall GPA',
'courses': 'Courses',
'version': 'Version',
'last_modified': 'Last Modified',
'format': {
'year': 'YYYY',
'year_month': 'MMM YYYY',
},
}

View File

@ -0,0 +1,9 @@
export default {
'title': {
'education': '教育经历',
},
'format': {
'year': 'YYYY',
'year_month': 'YYYY年MMM',
},
}