From 03216d148879b0ab752295b12c2e65ce3e5a29c2 Mon Sep 17 00:00:00 2001 From: Pomax Date: Sat, 8 Aug 2020 11:21:51 -0700 Subject: [PATCH] better localization --- chapters/whatis/interpolation.js | 34 ++++++-- lib/custom-element/api/graphics-api.js | 56 +++++++++++- locale-strings.js | 53 ------------ package.json | 3 +- tools/build.js | 7 +- tools/build/convert-markdown.js | 10 ++- tools/build/create-index-page.js | 35 +++----- tools/build/generate-lang-switcher.js | 12 +-- tools/build/generate-placeholders.js | 13 +-- tools/build/get-all-chapter-files.js | 3 +- tools/build/latex/cleanup.js | 22 +++-- tools/build/latex/latex-to-svg.js | 19 ++-- tools/build/markdown/inject-fallback.js | 12 ++- tools/build/process-locale.js | 16 ++-- tools/build/rewrite-graphics-element.js | 16 ++-- tools/locale-strings.js | 110 ++++++++++++++++++++++++ 16 files changed, 285 insertions(+), 136 deletions(-) delete mode 100644 locale-strings.js create mode 100644 tools/locale-strings.js diff --git a/chapters/whatis/interpolation.js b/chapters/whatis/interpolation.js index 67edc3dd..f5e869de 100644 --- a/chapters/whatis/interpolation.js +++ b/chapters/whatis/interpolation.js @@ -102,18 +102,42 @@ drawCurveCoordinates() { } onKeyDown() { + this.mark = false; if (this.keyboard[`ArrowDown`]) { - this.step--; - if (this.step < 10) this.step = 10; + this.stepDown(); } if (this.keyboard[`ArrowUp`]) { - this.step++; - if (this.step > 90) this.step = 90; + this.stepUp(); } redraw(); } +stepDown(value = 1) { + this.step -= value; + if (this.step < 10) this.step = 10; +} + +stepUp(value = 1) { + this.step += value; + if (this.step > 90) this.step = 90; +} + +onMouseDown() { + this.mark = this.cursor.y; +} + +onMouseUp() { + this.mark = false; +} + onMouseMove() { this.curve.update(); + + if (this.mark) { + let diff = this.mark - this.cursor.y, + mapped = map(diff, -this.height/2, this.height/2, 10, 90, true); + this.step = mapped | 0; + } + redraw(); -} \ No newline at end of file +} diff --git a/lib/custom-element/api/graphics-api.js b/lib/custom-element/api/graphics-api.js index 5bf0fa9e..4aa8a66e 100644 --- a/lib/custom-element/api/graphics-api.js +++ b/lib/custom-element/api/graphics-api.js @@ -236,7 +236,7 @@ class GraphicsAPI extends BaseAPI { * Add a plain point to the current shape */ vertex(x, y) { - this.currentShape.vertex({ x, y}); + this.currentShape.vertex({ x, y }); } /** @@ -244,7 +244,7 @@ class GraphicsAPI extends BaseAPI { */ end() { this.ctx.beginPath(); - let {x, y} = this.currentShape.first; + let { x, y } = this.currentShape.first; this.ctx.moveTo(x, y); this.currentShape.segments.forEach((s) => this[`draw${s.type}`](this.ctx, s.points, s.factor) @@ -279,6 +279,58 @@ class GraphicsAPI extends BaseAPI { this.line({ x: 0, y }, { x: this.width, y }); } } + + /** + * math functions + */ + floor(v) { + return Math.floor(v); + } + + ceil(v) { + return Math.ceil(v); + } + + round(v) { + return Math.round(v); + } + + abs(v) { + return Math.abs(v); + } + + sin(v) { + return Math.sin(v); + } + + cos(v) { + return Math.cos(v); + } + + tan(v) { + return Math.tan(v); + } + + sqrt(v) { + return Math.sqrt(v); + } + + atan2(dy, dx) { + return Math.atan2(dy, dx); + } + + pow(v, p) { + return Math.pow(v, p); + } + + map(v, s, e, ns, ne, constrain = false) { + const i1 = e - s, + i2 = ne - ns, + p = v - s; + let r = ns + (p * i2) / i1; + if (constrain) r = r < ns ? ns : r > ne ? ne : r; + return r; + } } export { GraphicsAPI, Bezier, Vector }; diff --git a/locale-strings.js b/locale-strings.js deleted file mode 100644 index 5967d7b3..00000000 --- a/locale-strings.js +++ /dev/null @@ -1,53 +0,0 @@ -export default { - "defaultLocale": "en-GB", - "title": { - "en-GB": "A Primer on Bézier Curves", - "ja-JP": "ベジェ曲線入門", - "zh-CN": "贝塞尔曲线底漆" - }, - "subtitle": { - "en-GB": "A free, online book for when you really need to know how to do Bézier things.", - "ja-JP": "A free, online book for when you really need to know how to do Bézier things.", - "zh-CN": "A free, online book for when you really need to know how to do Bézier things." - }, - "description": { - "en-GB": "A detailed explanation of Bézier curves, and how to do the many things that we commonly want to do with them.", - "ja-JP": "A detailed explanation of Bézier curves, and how to do the many things that we commonly want to do with them.", - "zh-CN": "A detailed explanation of Bézier curves, and how to do the many things that we commonly want to do with them." - }, - "tocLabel": { - "en-GB": "Table of Contents", - "ja-JP": "目次", - "zh-CN": "目录" - }, - "localeName": { - "en-GB": "English", - "zh-CN": "中文", - "ja-JP": "日本語" - }, - "langSwitchLabel": { - "en-GB": "Read this in your own language:", - "zh-CN": "Read this in your own language:", - "ja-JP": "Read this in your own language:" - }, - "langHelpLabel": { - "en-GB": "Don't see your language listed? Help translate this content!", - "zh-CN": "Don't see your language listed? Help translate this content!", - "ja-JP": "Don't see your language listed? Help translate this content!" - }, - "disabledMessage": { - "en-GB": "Scripts are disabled. Showing fallback image.", - "zh-CN": "Scripts are disabled. Showing fallback image.", - "ja-JP": "Scripts are disabled. Showing fallback image." - }, - "changelogTitle": { - "en-GB": "What's new?", - "zh-CN": "What's new?", - "ja-JP": "What's new?" - }, - "changelogDescription": { - "en-GB": "This primer is a living document, and so depending on when you last look at it, there may be new content. Click the following link to expand this section to have a look at what got added, when.", - "zh-CN": "This primer is a living document, and so depending on when you last look at it, there may be new content. Click the following link to expand this section to have a look at what got added, when.", - "ja-JP": "This primer is a living document, and so depending on when you last look at it, there may be new content. Click the following link to expand this section to have a look at what got added, when." - } -} diff --git a/package.json b/package.json index 5a71b865..c6e76f3f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "url": "https://github.com/Pomax/bezierinfo/issues" }, "scripts": { - "start": "run-s build", + "start": "run-s lint build", + "lint": "prettier ./tools --write", "build": "node ./tools/build.js", "test": "run-p server browser", "server": "http-server -p 8000 --cors", diff --git a/tools/build.js b/tools/build.js index 97b4734e..fb56f05b 100644 --- a/tools/build.js +++ b/tools/build.js @@ -1,3 +1,4 @@ +import LocaleStrings from "./locale-strings.js"; import getAllChapterFiles from "./build/get-all-chapter-files.js"; import processLocale from "./build/process-locale.js"; import createIndexPages from "./build/create-index-page.js"; @@ -9,9 +10,9 @@ import createIndexPages from "./build/create-index-page.js"; */ getAllChapterFiles().then((chapterFiles) => { const languageCodes = Object.keys(chapterFiles); - languageCodes.forEach(async (locale) => { - const chapters = await processLocale(locale, chapterFiles); - createIndexPages(locale, chapters, languageCodes); + const localeStrings = new LocaleStrings(locale); + const chapters = await processLocale(locale, localeStrings, chapterFiles); + createIndexPages(locale, localeStrings, chapters); }); }); diff --git a/tools/build/convert-markdown.js b/tools/build/convert-markdown.js index de69c5af..f2a2b276 100644 --- a/tools/build/convert-markdown.js +++ b/tools/build/convert-markdown.js @@ -8,14 +8,18 @@ nunjucks.configure(".", { autoescape: false }); /** * ...docs go here... */ -export default async function convertMarkDown(chapter, locale, markdown) { - markdown = injectGraphicsFallback(chapter, locale, markdown); +export default async function convertMarkDown( + chapter, + localeStrings, + markdown +) { + markdown = injectGraphicsFallback(chapter, localeStrings, markdown); const { data, latex } = extractLaTeX(markdown); await Promise.all( Object.keys(latex).map(async (key, pos) => { - const svg = await latexToSVG(latex[key], chapter, locale, pos + 1); + const svg = await latexToSVG(latex[key], chapter, localeStrings, pos + 1); return (latex[key] = svg); }) ); diff --git a/tools/build/create-index-page.js b/tools/build/create-index-page.js index 4740557d..075a20ca 100644 --- a/tools/build/create-index-page.js +++ b/tools/build/create-index-page.js @@ -3,28 +3,23 @@ import path from "path"; import prettier from "prettier"; import generateLangSwitcher from "./generate-lang-switcher.js"; import nunjucks from "nunjucks"; -import localeStrings from "../../locale-strings.js"; import sectionOrder from "../../chapters/toc.js"; import changelog from "../../changelog.js"; -const defaultLocale = localeStrings.defaultLocale; - nunjucks.configure(".", { autoescape: false }); /** * ...docs go here... */ -export default async function createIndexPages(locale, chapters, languages) { - let base = ``; - - if (locale !== defaultLocale) { - base = ``; - } - - const langSwitcher = generateLangSwitcher(locale, languages, defaultLocale); - +export default async function createIndexPages( + locale, + localeStrings, + chapters +) { + const defaultLocale = localeStrings.getDefaultLocale(); + const base = locale !== defaultLocale ? `` : ``; + const langSwitcher = generateLangSwitcher(localeStrings); const toc = {}; - const preface = `
${ chapters[sectionOrder[0]] }
`; @@ -40,10 +35,12 @@ export default async function createIndexPages(locale, chapters, languages) { }); let changeLogHTML = []; - Object.keys(changelog).forEach(period => { - let changes = changelog[period].map(change => `
  • ${change}
  • `).join(`\n`); + Object.keys(changelog).forEach((period) => { + let changes = changelog[period] + .map((change) => `
  • ${change}
  • `) + .join(`\n`); changeLogHTML.push(`

    ${period}

    `); - }) + }); // Set up the templating context const context = { @@ -57,11 +54,7 @@ export default async function createIndexPages(locale, chapters, languages) { }; // And inject all the relevant locale strings - Object.keys(localeStrings).forEach((key) => { - if (localeStrings[key][locale]) { - context[key] = localeStrings[key][locale]; - } - }); + localeStrings.extendContext(context); const index = nunjucks.render(`index.template.html`, context); diff --git a/tools/build/generate-lang-switcher.js b/tools/build/generate-lang-switcher.js index 4b3ad2ee..65174862 100644 --- a/tools/build/generate-lang-switcher.js +++ b/tools/build/generate-lang-switcher.js @@ -1,9 +1,8 @@ -import localeStrings from "../../locale-strings.js"; +export default function generateLangSwitcher(localeStrings) { + const defaultLocale = localeStrings.getDefaultLocale(); -const defaultLocale = localeStrings.defaultLocale; - -export default function generateLangSwitcher(currentLocale, allLocales) { - return allLocales + return localeStrings + .getAllLocaleCodes() .map((locale) => { let link; if (locale === defaultLocale) { @@ -11,7 +10,8 @@ export default function generateLangSwitcher(currentLocale, allLocales) { } else { link = `./${locale}/index.html`; } - return `
  • ${localeStrings.localeName[locale]}
  • `; + let localeName = localeStrings.getLocaleName(locale); + return `
  • ${localeName}
  • `; }) .join(`\n`); } diff --git a/tools/build/generate-placeholders.js b/tools/build/generate-placeholders.js index d4b3b288..c5481021 100644 --- a/tools/build/generate-placeholders.js +++ b/tools/build/generate-placeholders.js @@ -1,12 +1,12 @@ import fs from "fs-extra"; import path from "path"; import rewriteGraphicsElement from "./rewrite-graphics-element.js"; -import localeStrings from "../../locale-strings.js"; +import localeStrings from "../locale-strings.js"; const defaultLocale = localeStrings.defaultLocale; const moduleURL = new URL(import.meta.url); -const __dirname = path.dirname(moduleURL.href.replace(`file:///`,``)); +const __dirname = path.dirname(moduleURL.href.replace(`file:///`, ``)); export default async function generatePlaceHolders(locale, markdown) { if (locale !== defaultLocale) return; @@ -29,10 +29,12 @@ export default async function generatePlaceHolders(locale, markdown) { } while (pos !== -1); const keys = Object.keys(elements); - const sourcePaths = keys.map(key => elements[key].match(/src="([^"]+)"/)[1]); + const sourcePaths = keys.map( + (key) => elements[key].match(/src="([^"]+)"/)[1] + ); await Promise.all( - sourcePaths.map(async(srcPath, i) => { + sourcePaths.map(async (srcPath, i) => { try { // Get the sketch code const sourcePath = path.join(__dirname, "..", "..", srcPath); @@ -61,8 +63,9 @@ export default async function generatePlaceHolders(locale, markdown) { // console.log(`Writing placeholder to ${filename}`); fs.ensureDirSync(path.dirname(destPath)); fs.writeFileSync(filename, imageData); + } catch (e) { + console.error(e); } - catch (e) { console.error(e); } }) ); } diff --git a/tools/build/get-all-chapter-files.js b/tools/build/get-all-chapter-files.js index 118f0d11..acf1a55d 100644 --- a/tools/build/get-all-chapter-files.js +++ b/tools/build/get-all-chapter-files.js @@ -3,7 +3,7 @@ import path from "path"; // make sure we know what our base location is const moduleURL = new URL(import.meta.url); -const __dirname = path.dirname(moduleURL.href.replace(`file:///`,``)); +const __dirname = path.dirname(moduleURL.href.replace(`file:///`, ``)); const BASEDIR = path.join(__dirname, "..", ".."); /** @@ -11,7 +11,6 @@ const BASEDIR = path.join(__dirname, "..", ".."); */ export default /* async */ function getAllChapterFiles() { return new Promise((resolve, reject) => { - glob(path.join(BASEDIR, `chapters/**/content*md`), (err, files) => { if (err) reject(err); diff --git a/tools/build/latex/cleanup.js b/tools/build/latex/cleanup.js index e6e36254..dac12ee3 100644 --- a/tools/build/latex/cleanup.js +++ b/tools/build/latex/cleanup.js @@ -1,16 +1,20 @@ export default function cleanUp(latex) { // strip any \[ and \], which is a block-level LaTeX markup indicator for MathJax: - latex = latex.replace(/^'/,'').replace(/'$/,'').replace('\\[','').replace('\\]',''); + latex = latex + .replace(/^'/, "") + .replace(/'$/, "") + .replace("\\[", "") + .replace("\\]", ""); // wrap some known functor words in italics markup - ['Bézier'].forEach(term => { - latex = latex.replace(new RegExp(term, 'g'), '\\textit{' + term + '}'); + ["Bézier"].forEach((term) => { + latex = latex.replace(new RegExp(term, "g"), "\\textit{" + term + "}"); }); // also unindent the LaTeX. var indent = false; - var lines = latex.split('\n').filter(line => !!line.trim()); - var clean = function(line, idx) { + var lines = latex.split("\n").filter((line) => !!line.trim()); + var clean = function (line, idx) { if (!indent) { var matched = line.match(/^(\s+)/); if (matched) { @@ -18,10 +22,10 @@ export default function cleanUp(latex) { } } if (indent) { - lines[idx] = line.replace(indent,'').trim(); + lines[idx] = line.replace(indent, "").trim(); } - } + }; lines.forEach(clean); - latex = lines.join('\n'); + latex = lines.join("\n"); return latex; -}; +} diff --git a/tools/build/latex/latex-to-svg.js b/tools/build/latex/latex-to-svg.js index fb5a9acd..5266d3a3 100644 --- a/tools/build/latex/latex-to-svg.js +++ b/tools/build/latex/latex-to-svg.js @@ -47,7 +47,7 @@ export default async function latexToSVG(latex, chapter, locale, block) { "\\usepackage{xeCJK}", "\\xeCJKsetup{CJKmath=true}", "\\setCJKmainfont{gbsn00lp.ttf}", - ] + ]; } // The same goes for Japanese, although we obviously want a different @@ -69,13 +69,16 @@ export default async function latexToSVG(latex, chapter, locale, block) { `\\usepackage{color}`, `\\usepackage{amsmath}`, `\\usepackage{unicode-math}`, - ].concat(fonts).concat([ - `\\begin{document}`, - `\\[`, - cleanUp(latex), - `\\]`, - `\\end{document}`, - ]).join(`\n`) + ] + .concat(fonts) + .concat([ + `\\begin{document}`, + `\\[`, + cleanUp(latex), + `\\]`, + `\\end{document}`, + ]) + .join(`\n`) ); // Finally: run the conversion diff --git a/tools/build/markdown/inject-fallback.js b/tools/build/markdown/inject-fallback.js index 5f5b6209..e3a9326a 100644 --- a/tools/build/markdown/inject-fallback.js +++ b/tools/build/markdown/inject-fallback.js @@ -1,9 +1,13 @@ -import localeStrings from "../../../locale-strings.js"; - /** * ...docs go here... */ -export default function injectGraphicsFallback(chapter, locale, markdown) { +export default function injectGraphicsFallback( + chapter, + localeStrings, + markdown +) { + const translate = localeStrings.translate; + let pos = -1, data = markdown, startmark = ` - ${localeStrings.disabledMessage[locale]} + ${translate`disabledMessage`} `; } ); diff --git a/tools/build/process-locale.js b/tools/build/process-locale.js index 05670866..37be350a 100644 --- a/tools/build/process-locale.js +++ b/tools/build/process-locale.js @@ -4,12 +4,10 @@ import convertMarkDown from "./convert-markdown.js"; import generatePlaceHolders from "./generate-placeholders.js"; import nunjucks from "nunjucks"; import toc from "../../chapters/toc.js"; -import localeStrings from "../../locale-strings.js"; const moduleURL = new URL(import.meta.url); const __dirname = path.dirname(moduleURL.href.replace(`file:///`, ``)); -const defaultLocale = localeStrings.defaultLocale; const sectionList = toc.map((v) => path.posix.join( __dirname.split(path.sep).join(path.posix.sep), @@ -25,7 +23,14 @@ nunjucks.configure(".", { autoescape: false }); /** * ...docs go here... */ -export default async function processLocale(locale, chapterFiles) { +export default async function processLocale( + locale, + localeStrings, + chapterFiles +) { + const defaultLocale = localeStrings.getDefaultLocale(); + const translate = localeStrings.translate; + const localeFiles = chapterFiles[locale]; let localized = 0; let missing = 0; @@ -53,10 +58,7 @@ export default async function processLocale(locale, chapterFiles) { localeFiles.map(async (file) => { const chapter = file.match(/chapters\/([^/]+)\/content./)[1]; const markdown = fs.readFileSync(file).toString("utf8"); - const replaced = nunjucks.renderString(markdown, { - disableMessage: `${localeStrings.disabledMessage[locale]}`, - }); - const converted = await convertMarkDown(chapter, locale, replaced); + const converted = await convertMarkDown(chapter, localeStrings, markdown); chapters[chapter] = converted; generatePlaceHolders(locale, converted); // ← this is super fancy functionality. }) diff --git a/tools/build/rewrite-graphics-element.js b/tools/build/rewrite-graphics-element.js index 3d51a806..f3af449f 100644 --- a/tools/build/rewrite-graphics-element.js +++ b/tools/build/rewrite-graphics-element.js @@ -3,12 +3,12 @@ import performCodeSurgery from "../../lib/custom-element/lib/perform-code-surger import prettier from "prettier"; export default function rewriteGraphicsElement(code, width, height) { + const split = splitCodeSections(code); + const globalCode = split.quasiGlobal; + const classCode = performCodeSurgery(split.classCode); - const split = splitCodeSections(code); - const globalCode = split.quasiGlobal; - const classCode = performCodeSurgery(split.classCode); - - return prettier.format(` + return prettier.format( + ` import CanvasBuilder from 'canvas'; import { GraphicsAPI, Bezier, Vector } from "../../lib/custom-element/api/graphics-api.js"; @@ -33,5 +33,7 @@ export default function rewriteGraphicsElement(code, width, height) { const canvas = example.canvas; export { canvas }; - `, { parser: `babel` }); -}; + `, + { parser: `babel` } + ); +} diff --git a/tools/locale-strings.js b/tools/locale-strings.js new file mode 100644 index 00000000..6b80d1ba --- /dev/null +++ b/tools/locale-strings.js @@ -0,0 +1,110 @@ +const defaultLocale = "en-GB"; + +const localeStringData = { + title: { + "en-GB": "A Primer on Bézier Curves", + "ja-JP": "ベジェ曲線入門", + "zh-CN": "贝塞尔曲线底漆", + }, + subtitle: { + "en-GB": + "A free, online book for when you really need to know how to do Bézier things.", + }, + description: { + "en-GB": + "A detailed explanation of Bézier curves, and how to do the many things that we commonly want to do with them.", + }, + tocLabel: { + "en-GB": "Table of Contents", + "ja-JP": "目次", + "zh-CN": "目录", + }, + localeName: { + "en-GB": "English", + "zh-CN": "中文", + "ja-JP": "日本語", + }, + langSwitchLabel: { + "en-GB": "Read this in your own language:", + }, + langHelpLabel: { + "en-GB": + 'Don\'t see your language listed? Help translate this content!', + }, + disabledMessage: { + "en-GB": "Scripts are disabled. Showing fallback image.", + }, + changelogTitle: { + "en-GB": "What's new?", + }, + changelogDescription: { + "en-GB": + "This primer is a living document, and so depending on when you last look at it, there may be new content. Click the following link to expand this section to have a look at what got added, when.", + }, +}; + +class LocaleStrings { + constructor(locale) { + this.currentLocale = locale; + const strings = (this.strings = {}); + + Object.keys(localeStringData).forEach((id) => { + const map = localeStringData[id]; + if (typeof map !== "object") return; + const value = map[locale] ?? map[defaultLocale]; + if (!value) throw new Error(`unknown locale string id "${id}".`); + strings[id] = value; + + // temporary bug catcher: + Object.defineProperty(this, id, { + get: () => { + throw new Error(`cannot get localestring ${id} via property access`); + }, + }); + }); + } + // templating tags: + + get translate() { + return this.get.bind(this); + } + + // functions: + + get(id) { + return this.strings[id]; + } + + getDefaultLocale() { + return defaultLocale; + } + + getCurrentLocale() { + return this.currentLocale; + } + + getLocaleName(locale) { + const name = localeStringData.localeName[locale]; + if (!name) { + throw new Error(`Unknown locale "${locale}"`); + } + return name; + } + + getAllLocaleCodes() { + return Object.keys(localeStringData.title); + } + + extendContext(context) { + const strings = this.strings; + + Object.keys(strings).forEach((key) => { + if (context[key]) { + throw new Error(`Context already has key "${key}"!`); + } + context[key] = strings[key]; + }); + } +} + +export default LocaleStrings;