diff --git a/public/assets/images/flag-en.svg b/public/assets/images/flag-en.svg new file mode 100644 index 0000000000000000000000000000000000000000..be642b356a8353c5c91a2f03518bffc011d0faaf --- /dev/null +++ b/public/assets/images/flag-en.svg @@ -0,0 +1,75 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + id="svg1" + inkscape:version="1.0.2 (e86c870879, 2021-01-15)" + viewBox="0 0 20 15" + sodipodi:version="0.32" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:docname="en.svg" + version="1.1" + width="20" + height="15"> + <metadata id="metadata14"> + <rdf:RDF> + <cc:Work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <defs id="defs12" /> + <sodipodi:namedview pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1920" + inkscape:window-height="1010" + id="namedview10" + showgrid="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:zoom="32" + inkscape:cx="2.0440539" + inkscape:cy="2.5393561" + inkscape:window-x="2224" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg1" + inkscape:document-rotation="0" /> + <g id="g578" + transform="matrix(0.33348664,0,0,0.49980131,0.00398355,0.00382895)"> + <rect id="rect124" + style="fill:#000066;stroke-width:1pt" + height="30" + width="60" + y="0" + x="0" /> + <g id="g584"> + <path id="path146" + style="fill:#ffffff;stroke-width:1pt" + d="m 0,0 v 3.3541 l 53.292,26.646 H 60 v -3.354 L 6.708,1e-4 H 0 Z M 60,0 V 3.354 L 6.708,30 H 0 V 26.646 L 53.292,0 Z" /> + <path id="path136" + style="fill:#ffffff;stroke-width:1pt" + d="M 25,0 V 30 H 35 V 0 Z M 0,10 V 20 H 60 V 10 Z" /> + <path id="path141" + style="fill:#cc0000;stroke-width:1pt" + d="m 0,12 v 6 H 60 V 12 Z M 27,0 v 30 h 6 V 0 Z" /> + <path id="path150" + style="fill:#cc0000;stroke-width:1pt" + d="M 0,30 20,20 h 4.472 l -20,10 z M 0,0 20,10 H 15.528 L 0,2.2361 Z m 35.528,10 20,-10 H 60 L 40,10 Z M 60,30 40,20 h 4.472 L 60,27.764 Z" /> + </g> + </g> +</svg> diff --git a/public/assets/images/flag-fr.svg b/public/assets/images/flag-fr.svg new file mode 100644 index 0000000000000000000000000000000000000000..e8c6f9f6f37ed22f9ce49cc951c242cd3ff7c7f1 --- /dev/null +++ b/public/assets/images/flag-fr.svg @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="20" + height="15" + viewBox="0 0 5.2916665 3.96875" + version="1.1" + id="svg865" + inkscape:version="1.0.2 (e86c870879, 2021-01-15)" + sodipodi:docname="fr.svg"> + <defs id="defs859" /> + <sodipodi:namedview id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="256" + inkscape:cx="20.884772" + inkscape:cy="15.722232" + inkscape:document-units="mm" + inkscape:current-layer="layer1" + inkscape:document-rotation="0" + showgrid="false" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:window-width="1638" + inkscape:window-height="1010" + inkscape:window-x="2224" + inkscape:window-y="0" + inkscape:window-maximized="0" + units="px" /> + <metadata id="metadata862"> + <rdf:RDF> + <cc:Work rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g inkscape:label="Calque 1" + inkscape:groupmode="layer" + id="layer1" + transform="translate(-170.91496,-192.29281)"> + <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#0000c2;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.00492183;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers stroke fill;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000" + d="m 170.91496,192.29281 h 1.75965 v 3.96087 h -1.75965 z" + id="rect10" /> + <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.00492183;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers stroke fill;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000" + d="m 172.67461,192.29281 h 1.75964 v 3.96087 h -1.75964 z" + id="rect10-3" /> + <path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#be0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.0049432;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers stroke fill;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000" + d="m 174.43425,192.29281 h 1.77188 v 3.96773 h -1.77188 z" + id="rect10-3-6" /> + </g> +</svg> diff --git a/public/assets/images/tech_logos/apache.png b/public/assets/images/tech_logos/apache.png deleted file mode 100644 index 24c10f44733ae7c98647f08d031dfa383be38c13..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/apache.png and /dev/null differ diff --git a/public/assets/images/tech_logos/c.png b/public/assets/images/tech_logos/c.png deleted file mode 100644 index fc00d068b1255276a15503c4f055fc24f6f233a9..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/c.png and /dev/null differ diff --git a/public/assets/images/tech_logos/css.png b/public/assets/images/tech_logos/css.png deleted file mode 100644 index 499afaaab62a968d7ab1baf35b5e04154c92daba..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/css.png and /dev/null differ diff --git a/public/assets/images/tech_logos/docker.png b/public/assets/images/tech_logos/docker.png deleted file mode 100644 index 61228d658ea6d01bb2a99ca1b24cffe6d5bddcd1..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/docker.png and /dev/null differ diff --git a/public/assets/images/tech_logos/html.png b/public/assets/images/tech_logos/html.png deleted file mode 100644 index 5f78f2d438d0fd7e69d52834e0fa6e791838fd72..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/html.png and /dev/null differ diff --git a/public/assets/images/tech_logos/js.png b/public/assets/images/tech_logos/js.png deleted file mode 100644 index f768218684cdb1eec27793b14d49cbac1a1c4dba..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/js.png and /dev/null differ diff --git a/public/assets/images/tech_logos/linux.png b/public/assets/images/tech_logos/linux.png deleted file mode 100644 index 6226359e12db783f46ff21b7bf61fd76d9b42555..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/linux.png and /dev/null differ diff --git a/public/assets/images/tech_logos/mongodb.png b/public/assets/images/tech_logos/mongodb.png deleted file mode 100644 index b5f846655bfa31a5e5b54a352efb0055131e95c8..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/mongodb.png and /dev/null differ diff --git a/public/assets/images/tech_logos/mysql.png b/public/assets/images/tech_logos/mysql.png deleted file mode 100644 index 4371026f51f3c40bb1d72739f3c1bb230477418f..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/mysql.png and /dev/null differ diff --git a/public/assets/images/tech_logos/nginx.png b/public/assets/images/tech_logos/nginx.png deleted file mode 100644 index b3bf0f889fb07ecc2a31e5e0d2063c66fc418fc8..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/nginx.png and /dev/null differ diff --git a/public/assets/images/tech_logos/node.png b/public/assets/images/tech_logos/node.png deleted file mode 100644 index 854d7ed021bbce7b68682714bb3608e578d0b1ac..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/node.png and /dev/null differ diff --git a/public/assets/images/tech_logos/postgre.png b/public/assets/images/tech_logos/postgre.png deleted file mode 100644 index f0d08ae50cc49a56e3d111ccd5ed99f32ad9eb9a..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/postgre.png and /dev/null differ diff --git a/public/assets/images/tech_logos/python.png b/public/assets/images/tech_logos/python.png deleted file mode 100644 index 3d9e0c290514c05c2115a3d13748b48f500f1625..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/python.png and /dev/null differ diff --git a/public/assets/images/tech_logos/react.png b/public/assets/images/tech_logos/react.png deleted file mode 100644 index bdbb5f043f0875e0311c1134f65ac703b41402b7..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/react.png and /dev/null differ diff --git a/public/assets/images/tech_logos/rust.png b/public/assets/images/tech_logos/rust.png deleted file mode 100644 index 41a01113e7637228c9034fa4572f56f2eb681bcc..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/rust.png and /dev/null differ diff --git a/public/assets/images/tech_logos/sass.png b/public/assets/images/tech_logos/sass.png deleted file mode 100644 index 88d9104a9101d1c2a44349c4b2df038eef277bc6..0000000000000000000000000000000000000000 Binary files a/public/assets/images/tech_logos/sass.png and /dev/null differ diff --git a/public/assets/translations/en.json b/public/assets/translations/en.json new file mode 100644 index 0000000000000000000000000000000000000000..7dbe0401ad090f13686d140bc20ce64410f3a334 --- /dev/null +++ b/public/assets/translations/en.json @@ -0,0 +1,33 @@ +{ + "Simplicité": "Simplicity", + "Légèreté": "Lightness", + "Écologie": "Ecology", + "kuadrado-home-description": "Video game creation studio based in France - Ardèche<br />Digital art creation | Development of free and open source software tools | Education", + "Site en construction ...": "Website in construction...", + "Sur les réseaux": "On the networks", + "kuadrado-footer-copyleft": "All images on this website were made by me and may be reused for personal usage.", + "Ce site web est": "This website is", + "Jeux": "Games", + "Pédagogie": "Education", + "games-description": "Video game creations, web games and PC games, work in progress.", + "education-description": "Taking ownership of technology through knowledge sharing.", + "software-description": "R&D, experimental projects, web and software tools", + "games-page-intro": "Creation of independent video games - web games, PC games and projects in development", + "Jouer": "Play", + "edu-page-intro": "Workshops, courses, workshops and private lessons accessible to all. Programming, 2D graphics, video games, computer popularisation, etc.", + "Programmation": "Programming", + "edu-learn-coding": "<b>Break the code wall!</b><br />Learn to program with different languages (Python, Javascript, ...), for web, software, video game or other.", + "Dessin numérique et animation 2D": "Digital drawing and 2D animation", + "edu-learn-2d": "Learn how to use open source 2D graphics and animation software to create characters and backgrounds, and run your own cartoon, illustration or video game project.", + "Maths et physiques": "Math and physics", + "edu-learn-math": "Tackle the fundamental concepts in a relaxed way for the pleasure of understanding based on the application areas of video games.", + "Aide informatique générale": "General IT support", + "edu-learn-computer": "Lost with your computer or smartphone, software, internet? Get to grips with the basics and learn step by step how to use technology serenely.", + "Stage GNU/Linux": "GNU/Linux course", + "edu-learn-gnu": "<b>Take the free route! </b><br/>Learn how to install Linux, take your first steps with free software and gain enough autonomy for basic use.", + "Créer un jeu avec Mentalo": "Create a game with Mentalo", + "edu-learn-mentalo": "Create a game in a few sessions with the Mentalo application. Handle logical, narrative and artistic concepts with maximum simplicity.", + "Me contacter": "Contact me", + "Pour s'inscrire ou en savoir plus": "To register or find out more", + "software-page-intro": "R&D, experimental projects, software tools for game development or for the web." +} \ No newline at end of file diff --git a/public/assets/translations/fr.json b/public/assets/translations/fr.json new file mode 100644 index 0000000000000000000000000000000000000000..fdcaa31909af1067f2f13dbe733496b97e59ed30 --- /dev/null +++ b/public/assets/translations/fr.json @@ -0,0 +1,16 @@ +{ + "kuadrado-home-description": "Studio de création de jeux vidéo basé en Ardèche, Vernoux en Vivarais.<br />Création artisitique numérique | Développement d'outillage logiciel libre et open source | Pédagogie.", + "kuadrado-footer-copyleft": "Toutes les images du site ont été réalisées par mes soins et peuvent être réutilisées pour un usage personnel.", + "games-description": "Créations vidéoludiques, jeux web et jeux PC, projets en cours.", + "education-description": "S'approprier la technologie par le partage de connaissances.", + "software-description": "R&D, projets expérimentaux, web et outillage logiciel", + "games-page-intro": "Création de jeux vidéos indépendants.<br/>Jeux web, PC et projets en cours de développement", + "edu-page-intro": "Ateliers, stages, workshops et cours particuliers accessibles à tous. Programmation, graphisme 2D, jeux vidéo, vulgarisation informatique, etc.", + "edu-learn-coding": "<b>Franchissez le mur du code !</b><br />Apprenez à programmer avec différents langages (Python, Javascript, ...), pour du web, du logiciel, du jeu vidéo ou autre.", + "edu-learn-2d": "Apprenez à utiliser des logiciels libres de création graphique 2D et d'animation.<br />Créez des personnages et des décors, menez votre projet de dessin animé, d'illustration ou de jeu vidéo.", + "edu-learn-math": "Abordez les notions fondamentales de façon décontractée pour le plaisir de comprendre en s'appuyant sur les domaines d'application du jeu vidéo.", + "edu-learn-computer": "Perdu avec votre ordinateur ou votre smartphone, les logiciels, internet ? Prenez en main les fondamentaux apprenez pas à pas à utiliser sereinement la technologie.", + "edu-learn-gnu": "<b>Passez le cap du libre ! </b><br/>Apprenez à installer Linux, faites vos premiers pas avec les logiciels libres et acquérez une autonomie suffisante pour une utilisation basique.", + "edu-learn-mentalo": "Créez un jeu en quelques séances avec l'application Mentalo. Manipulez des concepts logiques, narratifs et artistiques avec le maximum de simplicité.", + "software-page-intro": "R&D, projets expérimentaux, outillage logiciel pour le développement de jeu ou pour le web." +} \ No newline at end of file diff --git a/public/education/education.js b/public/education/education.js index f6ccf79699a659c430e4878c7de49bfee751109e..8207c7e067fad0161f321ccd291908f1a0f00a0d 100644 --- a/public/education/education.js +++ b/public/education/education.js @@ -10,10 +10,199 @@ module.exports = { module.exports = { images_url: `/assets/images`, data_url: `/assets/data`, + translations_url: "/assets/translations" }; },{}],3:[function(require,module,exports){ "use strict"; +/** + * A tiny library to handle text translation. + */ + +/** + * init parameter object type + * @typedef TranslatorParam + * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes + * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en" + * + * @property {Boolean} use_url_locale_fragment - DEFAULT: true - + * If true, the 1st fragment of the window.location url will be parsed as a language code + * Example: + * window.location + * > https://example.com/en/some-page... + * then the /en/ part of the url will be taken in priority and locale will be set to "en". + * window.location + * > https://example.com/some-page... + * No locale fragment will be found so locale will not be set from url fragment. + * window.location + * > https://example.com/some-page/en + * Doesn't work, locale fragment must be the first framgment + * + * @property {String} local_storage_key - DEFAULT: "translator-prefered-language" + * The key used to saved the current locale into local storage + * + * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations + * Translations files are expected to be named with their corresponding locale code. + * Example: + * if supported_languages is set to ["en", "fr", "it"] + * and translations_url is set to "https://example.com/translations/"" + * Then the expected translations files are + * https://example.com/translations/en.json + * https://example.com/translations/fr.json + * https://example.com/translations/it.json + * The json resources must simple key value maps, value being the translated text. + */ + +module.exports = { + locale: "en", // ISO 639 lowercase language code + supported_languages: ["en"], + translations: {}, + translations_url: "", + use_url_locale_fragment: true, + local_storage_key: "translator-prefered-language", + + /** + * Initialize the lib with params + * @param {TranslatorParam} params + * @returns {Promise} + */ + init(params) { + Object.entries(params).forEach(k_v => { + const [key, value] = k_v; + if ([ + "supported_languages", + "use_url_locale_fragment", + "local_storage_key", + "translations_url" + ].includes(key)) { + this[key] = value; + } + }); + + this.translations_url = this.format_translations_url(this.translations_url); + this.supported_languages = this.format_supported_languages(this.supported_languages); + + return new Promise((resolve, reject) => { + const loc = + (() => {// Locale from url priority 1 + if (this.use_url_locale_fragment) { + const first_url_fragment = window.location.pathname.substring(1).split("/")[0]; + const fragment_is_locale = this.supported_languages.includes(first_url_fragment); + return fragment_is_locale ? first_url_fragment : ""; + } else { + return ""; + } + })() + || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2 + || (() => { // locale from navigator priority 3 + const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase(); + return this.supported_languages.includes(navigator_locale) + ? navigator_locale + : this.supported_languages[0] // Default if navigator locale is not supported + })(); + + fetch(`${this.translations_url}${loc}.json`) + .then(response => response.json()) + .then(response => { + this.locale = loc; + this.translations = response; + resolve(); + }) + .catch(err => { + this.locale = "en"; + reject(err); + }); + }); + }, + + /** + * Return a lowercase string without dash. + * If given locale is en-EN, then "en" will be returned. + * @param {String} locale + * @returns A lowercase string + */ + format_locale(locale) { + return locale.split("-")[0].toLowerCase(); + }, + + /** + * Appends a slash at the end of the given string if missing, and returns the url. + * @param {String} url + * @returns {String} + */ + format_translations_url(url) { + if (url.charAt(url.length - 1) !== "/") { + url += "/" + } + return url; + }, + + /** + * Return the array of language codes formatted as lowsercase language code. + * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned. + * @param {String[]} languages_codes + * @returns {String[]} + */ + format_supported_languages(languages_codes) { + return languages_codes.map(lc => this.format_locale(lc)); + }, + + /** + * Fetches a new set of translations in case the wanted language changes + * @param {String} locale A lowercase language code + * @returns {Promise} + */ + update_translations(locale) { + locale = this.format_locale(locale); + return new Promise((resolve, reject) => { + fetch(`${this.translations_url}${locale}.json`) + .then(response => response.json()) + .then(response => { + this.translations = response; + this.locale = locale; + + localStorage.setItem(this.local_storage_key, locale); + + const split_path = window.location.pathname.substring(1).split("/"); + const first_url_fragment = split_path[0]; + const fragment_is_locale = this.supported_languages.includes(first_url_fragment); + + if (fragment_is_locale) { + split_path.splice(0, 1, locale); + const updated_path = split_path.join("/"); + window.history.replaceState(null, "", "/" + updated_path); + } + resolve(); + }) + .catch(err => { + reject(err); + }); + }); + }, + + /** + * Tries to get the translation of the source string, or return the string as it. + * @param {String} text The source text to translate + * @param {Object} params Some dynamic element to insert in the text + * Params can be used if the translated text provide placeholder like {%some_word%} + * Example: + * translator.trad("Some trad key", {some_word: "a dynamic parameter"}) + * -> translation for "Some trad key": "A translated text with {%some_word%}" + * -> will be return as : "A translated text with a dynamic parameter" + * @returns {String} + */ + trad: function (text, params = {}) { + text = this.translations[text] || text; + + Object.keys(params).forEach(k => { + text = text.replace(`{%${k}%}`, params[k]); + }); + + return text; + } +}; +},{}],4:[function(require,module,exports){ +"use strict"; module.exports = { register_key: "objectToHtmlRender", @@ -229,70 +418,72 @@ module.exports = { window.dispatchEvent(event); }, }; -},{}],4:[function(require,module,exports){ +},{}],5:[function(require,module,exports){ "use strict"; +const translator = require("ks-cheap-translator"); +const { translations_url } = require("../../constants"); class WebPage { constructor(args) { Object.assign(this, args); + + if (!this.id) { + this.id = "webpage-" + performance.now(); + } + + translator.init({ + translations_url, + supported_languages: ["fr", "en"], + }).then(this.refresh_all.bind(this)); + } + + refresh() { + obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) + } + + refresh_all() { + obj2htm.renderCycle() } } module.exports = WebPage; -},{}],5:[function(require,module,exports){ +},{"../../constants":2,"ks-cheap-translator":3}],6:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../constants"); const WebPage = require("../../lib/web-page"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); const EDU_THEMES = [ - // { - // title: "Création de jeux vidéo", - // description: "Conception, graphisme et animation, programmation, je vous accompagne dans la découverte des techniques pour créer un jeu vidéo de A à Z", - // image: "learning_theme_conception.png", - // pageUrl: "gamedev", - // }, { title: "Programmation", - description: `<b>Franchissez le mur du code !</b><br /> - Apprenez à programmer avec différents langages (Python, Javascript, C ...), pour du web, du logiciel, du jeu vidéo ou autre.`, + description: "edu-learn-coding", image: "learning_theme_coding.png", - // pageUrl: "coding", }, { title: "Dessin numérique et animation 2D", - description: `Apprenez à utiliser des logiciels libres de création graphique 2D et d'animation.<br /> - Créez des personnages et des décors, menez votre projet de dessin animé, d'illustration ou de jeu vidéo.`, + description: "edu-learn-2d", image: "learning_theme_2d.png", - // pageUrl: "2d", }, { title: "Maths et physique", - description: "Abordez les notions fondamentales de façon décontractée pour le plaisir de comprendre en s'appuyant sur les domaines d'application du jeu vidéo.", + description: "edu-learn-math", image: "learning_theme_math.png", - // pageUrl: "math", }, - // { - // title: "Musique et sons électroniques", - // description: "Découvrez des logiciels libres de composition musicales, de synthèse sonore et de prise de son.", - // image: "learning_theme_sound.png", - // pageUrl: "sound", - // }, { title: "Aide informatique générale", - description: "Perdu avec votre ordinateur ou votre smartphone, les logiciels, internet ? Prenez en main les fondamentaux apprenez pas à pas à utiliser sereinement la technologie.", + description: "edu-learn-computer", image: "learning_theme_pc.png", - // pageUrl: "popularization" }, { title: "Stage GNU/Linux", - description: `<b>Passez le cap du libre ! </b><br/> - Apprenez à installer Linux, faites vos premiers pas avec les logiciels libres et acquérez une autonomie suffisante pour une utilisation basique.`, + description: "edu-learn-gnu", image: "learning_theme_linux.png" }, { title: "Créer un jeu avec Mentalo", - description: "Créez un jeu en quelques séances avec l'application Mentalo. Manipulez des concepts logiques, narratifs et artistiques avec le maximum de simplicité.", + description: "edu-learn-mentalo", image: "learning_theme_mentalo.png", } ]; @@ -323,11 +514,10 @@ class EducationPage extends WebPage { }, ], }, - { tag: "h1", contents: "Pédagogie" }, + { tag: "h1", contents: t("Pédagogie") }, { tag: "p", - contents: `Ateliers, stages, workshops et cours particuliers accessibles à tous. - Programmation, graphisme 2D, jeux vidéo, vulgarisation informatique, etc.`, + contents: t("edu-page-intro"), }, ], }, @@ -354,8 +544,8 @@ class EducationPage extends WebPage { class: "edu-theme", contents: [ { tag: "img", width: 250, height: 140, class: "pixelated", src: `${images_url}/${theme.image}` }, - { tag: "h3", contents: theme.title }, - { tag: "p", contents: theme.description }, + { tag: "h3", contents: t(theme.title) }, + { tag: "p", contents: t(theme.description) }, ] } }) @@ -372,131 +562,11 @@ class EducationPage extends WebPage { tag: "div", class: "page-contents-center", contents: [ - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Pour qui ?" }, - // { - // tag: "p", - // class: "info-body", - // contents: `Les ateliers sont accessibles aux adultes comme aux enfants, plutôt à partir de 12 ans.<br/> - // Les séances ont lieu en groupes mixtes. Capacité limitée à 5 personnes. - // ` - // } - // ] - // }, - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Où ça ?" }, - // { - // tag: "p", - // class: "info-body", - // contents: "Dans mon local professionnel : <br /><blue>32 rue Simon Vialet, passage du Cheminou, 07240 Vernoux en Vivarais.</blue>" - // } - // ] - // }, - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Quel matériel ?" }, - // { - // tag: "p", - // class: "info-body", - // contents: `Le matériel informatique est fourni sur place (ordinateurs et tablettes graphique) - // mais il est possible d'amener le sien. - // <br />Il est recommandé d'apporter au moins une clé USB pour faire ses sauvegardes.` - // } - // ] - // }, - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Quand ?" }, - // { - // tag: "ul", - // class: "info-body tabled", - // contents: [ - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Mardi" }, - // { tag: "span", contents: "16h - 18h" }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Mercredi" }, - // { tag: "span", contents: "14h - 16h" }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Jeudi" }, - // { tag: "span", contents: "16h - 18h" }, - // ] - // }, - // { - // tag: "li", - // class: "fullwidth", - // contents: "<em><blue>Ouvert de Septembre à Juin, sauf vacances scolaires ou fermetures exceptionnelles</blue></em>" - // } - // ] - // }, - // ] - // }, - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Combien ça coûte ?" }, - // { - // tag: "ul", - // class: "info-body tabled", - // contents: [ - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Inscription au mois" }, - // { tag: "span", contents: "50€, accès à toutes les plages horaires." }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Inscription à la séance" }, - // { tag: "span", contents: "15€" }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Cours particuliers" }, - // { tag: "span", contents: "30€/h, sur place ou en visio. Horaires à définir." }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Stage 4 séances de 2h" }, - // { tag: "span", contents: "40€ par personne, horaires et dates à définir selon la demande." }, - // ] - // } - // ] - // } - // ] - // }, { tag: "div", class: "info-block", contents: [ - { tag: "h3", class: "info-title", contents: "Pour s'inscrire ou en savoir plus <em>(programme 2021 2022 à définir, plus d'infos bientôt)</em>" }, + { tag: "h3", class: "info-title", contents: `${t("Pour s'inscrire ou en savoir plus")} <em>(programme 2022 à définir, plus d'infos bientôt)</em>` }, { tag: "ul", class: "info-body", @@ -504,7 +574,7 @@ class EducationPage extends WebPage { { tag: "li", contents: [ - { tag: "span", contents: "Me contacter" }, + { tag: "span", contents: t("Me contacter") }, { tag: "a", href: "mailto:contact@kuadrado-software.fr", @@ -524,12 +594,6 @@ class EducationPage extends WebPage { }, ] }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "ou passer directement me voir au local !" } - // ] - // } ] } ] @@ -546,13 +610,13 @@ class EducationPage extends WebPage { module.exports = EducationPage; -},{"../../../constants":2,"../../lib/web-page":4}],6:[function(require,module,exports){ +},{"../../../constants":2,"../../lib/web-page":5,"ks-cheap-translator":3}],7:[function(require,module,exports){ "use strict"; const runPage = require("../../run-page"); const EducationPage = require("./education"); runPage(EducationPage); -},{"../../run-page":7,"./education":5}],7:[function(require,module,exports){ +},{"../../run-page":8,"./education":6}],8:[function(require,module,exports){ "use strict"; const renderer = require("object-to-html-renderer") @@ -565,19 +629,18 @@ module.exports = function runPage(PageComponent) { obj2htm.renderCycle(); }; -},{"./template/template":9,"object-to-html-renderer":3}],8:[function(require,module,exports){ +},{"./template/template":10,"object-to-html-renderer":4}],9:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../constants"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); const NAV_MENU_ITEMS = [ { url: "/games/", text: "Jeux" }, { url: "/education/", text: "Pédagogie", - // submenu: [ - // { url: "/gamedev", text: "Création de jeux vidéo" }, - // ] }, { url: "/software-development/", text: "Software" } ]; @@ -603,6 +666,12 @@ class NavBar { }); } + handle_chang_lang(lang) { + translator.update_translations(lang).then(() => { + obj2htm.renderCycle(); + }).catch(err => console.log(err)); + } + renderHome() { return { tag: "div", @@ -644,10 +713,20 @@ class NavBar { { tag: "a", href, - contents: text, + contents: t(text), }, ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), }; + }).concat({ + tag: "li", + class: "lang-flags", + contents: ["fr", "en"].map(lang => { + return { + tag: "img", src: `${images_url}/flag-${lang}.svg`, + class: translator.locale === lang ? "selected" : "", + onclick: this.handle_chang_lang.bind(this, lang) + } + }) }), }; } @@ -675,12 +754,14 @@ class NavBar { module.exports = NavBar; -},{"../../../constants":2}],9:[function(require,module,exports){ +},{"../../../constants":2,"ks-cheap-translator":3}],10:[function(require,module,exports){ "use strict"; const { in_construction } = require("../../config"); const { images_url } = require("../../constants"); const NavBar = require("./components/navbar"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator) class Template { constructor(props) { @@ -701,7 +782,7 @@ class Template { { tag: "strong", class: "page-contents-center", - contents: "Site en construction ...", + contents: t("Site en construction ..."), }, ], }, @@ -751,7 +832,7 @@ class Template { contents: [ { tag: "strong", - contents: "<blue>Sur les réseaux : </blue>", + contents: `<blue>${t("Sur les réseaux")} : </blue>`, }, { tag: "a", @@ -773,11 +854,11 @@ class Template { tag: "span", contents: `Copyleft 🄯 ${new Date() .getFullYear()} Kuadrado Software | - Toutes les images du site ont été réalisées par mes soins et peuvent être réutilisées pour un usage personnel.`, + ${t("kuadrado-footer-copyleft")}`, }, { tag: "div", contents: [ - { tag: "span", contents: "Ce site web est " }, + { tag: "span", contents: t("Ce site web est") + " " }, { tag: "a", target: "_blank", style_rules: { fontWeight: "bold" }, @@ -795,4 +876,4 @@ class Template { module.exports = Template; -},{"../../config":1,"../../constants":2,"./components/navbar":8}]},{},[6]); +},{"../../config":1,"../../constants":2,"./components/navbar":9,"ks-cheap-translator":3}]},{},[7]); diff --git a/public/games/games.js b/public/games/games.js index ccc7535972968c2ee9e0a8ac5bd066deea800b3d..e6819c85c7dcfdf8808549806a780b33d5c22cef 100644 --- a/public/games/games.js +++ b/public/games/games.js @@ -14,10 +14,199 @@ module.exports = { module.exports = { images_url: `/assets/images`, data_url: `/assets/data`, + translations_url: "/assets/translations" }; },{}],4:[function(require,module,exports){ "use strict"; +/** + * A tiny library to handle text translation. + */ + +/** + * init parameter object type + * @typedef TranslatorParam + * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes + * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en" + * + * @property {Boolean} use_url_locale_fragment - DEFAULT: true - + * If true, the 1st fragment of the window.location url will be parsed as a language code + * Example: + * window.location + * > https://example.com/en/some-page... + * then the /en/ part of the url will be taken in priority and locale will be set to "en". + * window.location + * > https://example.com/some-page... + * No locale fragment will be found so locale will not be set from url fragment. + * window.location + * > https://example.com/some-page/en + * Doesn't work, locale fragment must be the first framgment + * + * @property {String} local_storage_key - DEFAULT: "translator-prefered-language" + * The key used to saved the current locale into local storage + * + * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations + * Translations files are expected to be named with their corresponding locale code. + * Example: + * if supported_languages is set to ["en", "fr", "it"] + * and translations_url is set to "https://example.com/translations/"" + * Then the expected translations files are + * https://example.com/translations/en.json + * https://example.com/translations/fr.json + * https://example.com/translations/it.json + * The json resources must simple key value maps, value being the translated text. + */ + +module.exports = { + locale: "en", // ISO 639 lowercase language code + supported_languages: ["en"], + translations: {}, + translations_url: "", + use_url_locale_fragment: true, + local_storage_key: "translator-prefered-language", + + /** + * Initialize the lib with params + * @param {TranslatorParam} params + * @returns {Promise} + */ + init(params) { + Object.entries(params).forEach(k_v => { + const [key, value] = k_v; + if ([ + "supported_languages", + "use_url_locale_fragment", + "local_storage_key", + "translations_url" + ].includes(key)) { + this[key] = value; + } + }); + + this.translations_url = this.format_translations_url(this.translations_url); + this.supported_languages = this.format_supported_languages(this.supported_languages); + + return new Promise((resolve, reject) => { + const loc = + (() => {// Locale from url priority 1 + if (this.use_url_locale_fragment) { + const first_url_fragment = window.location.pathname.substring(1).split("/")[0]; + const fragment_is_locale = this.supported_languages.includes(first_url_fragment); + return fragment_is_locale ? first_url_fragment : ""; + } else { + return ""; + } + })() + || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2 + || (() => { // locale from navigator priority 3 + const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase(); + return this.supported_languages.includes(navigator_locale) + ? navigator_locale + : this.supported_languages[0] // Default if navigator locale is not supported + })(); + + fetch(`${this.translations_url}${loc}.json`) + .then(response => response.json()) + .then(response => { + this.locale = loc; + this.translations = response; + resolve(); + }) + .catch(err => { + this.locale = "en"; + reject(err); + }); + }); + }, + + /** + * Return a lowercase string without dash. + * If given locale is en-EN, then "en" will be returned. + * @param {String} locale + * @returns A lowercase string + */ + format_locale(locale) { + return locale.split("-")[0].toLowerCase(); + }, + + /** + * Appends a slash at the end of the given string if missing, and returns the url. + * @param {String} url + * @returns {String} + */ + format_translations_url(url) { + if (url.charAt(url.length - 1) !== "/") { + url += "/" + } + return url; + }, + + /** + * Return the array of language codes formatted as lowsercase language code. + * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned. + * @param {String[]} languages_codes + * @returns {String[]} + */ + format_supported_languages(languages_codes) { + return languages_codes.map(lc => this.format_locale(lc)); + }, + + /** + * Fetches a new set of translations in case the wanted language changes + * @param {String} locale A lowercase language code + * @returns {Promise} + */ + update_translations(locale) { + locale = this.format_locale(locale); + return new Promise((resolve, reject) => { + fetch(`${this.translations_url}${locale}.json`) + .then(response => response.json()) + .then(response => { + this.translations = response; + this.locale = locale; + + localStorage.setItem(this.local_storage_key, locale); + + const split_path = window.location.pathname.substring(1).split("/"); + const first_url_fragment = split_path[0]; + const fragment_is_locale = this.supported_languages.includes(first_url_fragment); + + if (fragment_is_locale) { + split_path.splice(0, 1, locale); + const updated_path = split_path.join("/"); + window.history.replaceState(null, "", "/" + updated_path); + } + resolve(); + }) + .catch(err => { + reject(err); + }); + }); + }, + + /** + * Tries to get the translation of the source string, or return the string as it. + * @param {String} text The source text to translate + * @param {Object} params Some dynamic element to insert in the text + * Params can be used if the translated text provide placeholder like {%some_word%} + * Example: + * translator.trad("Some trad key", {some_word: "a dynamic parameter"}) + * -> translation for "Some trad key": "A translated text with {%some_word%}" + * -> will be return as : "A translated text with a dynamic parameter" + * @returns {String} + */ + trad: function (text, params = {}) { + text = this.translations[text] || text; + + Object.keys(params).forEach(k => { + text = text.replace(`{%${k}%}`, params[k]); + }); + + return text; + } +}; +},{}],5:[function(require,module,exports){ +"use strict"; const MtlAnimation = require("./model/animation"); const MentaloEngine = require("./mentalo-engine"); @@ -45,7 +234,7 @@ module.exports = { color_tools, shape_tools, }; -},{"./lib/color-tools":5,"./lib/font-tools":6,"./lib/frame-rate-controller":7,"./lib/shape-tools":8,"./mentalo-engine":9,"./model/animation":10,"./model/choice":11,"./model/game":13,"./model/game-object":12,"./model/scene":17,"./model/scene-types":16,"./model/sound-track":18}],5:[function(require,module,exports){ +},{"./lib/color-tools":6,"./lib/font-tools":7,"./lib/frame-rate-controller":8,"./lib/shape-tools":9,"./mentalo-engine":10,"./model/animation":11,"./model/choice":12,"./model/game":14,"./model/game-object":13,"./model/scene":18,"./model/scene-types":17,"./model/sound-track":19}],6:[function(require,module,exports){ "use strict"; /** * Helpers to work with color values @@ -152,7 +341,7 @@ module.exports = { rgba_array_to_hex, same_rgba, }; -},{}],6:[function(require,module,exports){ +},{}],7:[function(require,module,exports){ "use strict"; /** * Helpers to works with default web fonts @@ -277,7 +466,7 @@ module.exports = { FONT_STYLE_OPTIONS, FONT_WEIGHT_OPTIONS }; -},{}],7:[function(require,module,exports){ +},{}],8:[function(require,module,exports){ "use strict"; /** @@ -315,7 +504,7 @@ class FrameRateController { } module.exports = FrameRateController; -},{}],8:[function(require,module,exports){ +},{}],9:[function(require,module,exports){ "use strict"; /** @@ -375,7 +564,7 @@ function draw_rect(ctx, x, y, width, height, options = { module.exports = { draw_rect, } -},{}],9:[function(require,module,exports){ +},{}],10:[function(require,module,exports){ "use strict"; const MtlGame = require("./model/game"); @@ -666,7 +855,7 @@ class MentaloEngine { } module.exports = MentaloEngine; -},{"./model/game":13,"./model/scene-types":16,"./render/render":19,"./translation":20}],10:[function(require,module,exports){ +},{"./model/game":14,"./model/scene-types":17,"./render/render":20,"./translation":21}],11:[function(require,module,exports){ "use strict"; const Loadable = require("./loadable"); @@ -746,7 +935,7 @@ class MtlAnimation extends Loadable { } module.exports = MtlAnimation; -},{"./loadable":15}],11:[function(require,module,exports){ +},{"./loadable":16}],12:[function(require,module,exports){ "use strict"; /** * The data type used for the choices of a MtlScene @@ -780,7 +969,7 @@ class MtlChoice { } module.exports = MtlChoice; -},{}],12:[function(require,module,exports){ +},{}],13:[function(require,module,exports){ "use strict"; const Loadable = require("./loadable"); @@ -834,7 +1023,7 @@ class MtlGameObject extends Loadable { } module.exports = MtlGameObject; -},{"./loadable":15}],13:[function(require,module,exports){ +},{"./loadable":16}],14:[function(require,module,exports){ "use strict"; const MtlScene = require("./scene"); @@ -1092,7 +1281,7 @@ class MtlGame { } module.exports = MtlGame; -},{"./scene":17,"./scene-types":16}],14:[function(require,module,exports){ +},{"./scene":18,"./scene-types":17}],15:[function(require,module,exports){ "use strict"; /** @@ -1134,7 +1323,7 @@ class LoadableGroup { } module.exports = LoadableGroup; -},{}],15:[function(require,module,exports){ +},{}],16:[function(require,module,exports){ "use strict"; /** @@ -1194,7 +1383,7 @@ class Loadable { } module.exports = Loadable; -},{}],16:[function(require,module,exports){ +},{}],17:[function(require,module,exports){ /** * An enum like object to describe the 2 possible type that can have a MtlScene */ @@ -1203,7 +1392,7 @@ module.exports = { PLAYABLE: "Playable", CINEMATIC: "Cinematic", }; -},{}],17:[function(require,module,exports){ +},{}],18:[function(require,module,exports){ "use strict"; const SCENE_TYPES = require("./scene-types"); @@ -1288,7 +1477,7 @@ class MtlScene extends LoadableGroup { } module.exports = MtlScene; -},{"./animation":10,"./choice":11,"./game-object":12,"./loadable-group":14,"./scene-types":16,"./sound-track":18}],18:[function(require,module,exports){ +},{"./animation":11,"./choice":12,"./game-object":13,"./loadable-group":15,"./scene-types":17,"./sound-track":19}],19:[function(require,module,exports){ "use strict"; const Loadable = require("./loadable"); @@ -1349,7 +1538,7 @@ class MtlSoundTrack extends Loadable { } module.exports = MtlSoundTrack; -},{"./loadable":15}],19:[function(require,module,exports){ +},{"./loadable":16}],20:[function(require,module,exports){ "use strict"; const FrameRateController = require("../lib/frame-rate-controller"); @@ -2292,7 +2481,7 @@ class MtlRender { } module.exports = MtlRender; -},{"../lib/color-tools":5,"../lib/font-tools":6,"../lib/frame-rate-controller":7,"../model/scene-types":16,"../ui-components/choice-cpt":21,"../ui-components/choices-panel-cpt":22,"../ui-components/closing-icon-cpt":23,"../ui-components/game-object-cpt":24,"../ui-components/inventory-cpt":25,"../ui-components/inventory-object-cpt":26,"../ui-components/inventory-slot-cpt":27,"../ui-components/scene-animation-cpt":28,"../ui-components/text-box-cpt":29,"../ui-components/user-error-popup":31}],20:[function(require,module,exports){ +},{"../lib/color-tools":6,"../lib/font-tools":7,"../lib/frame-rate-controller":8,"../model/scene-types":17,"../ui-components/choice-cpt":22,"../ui-components/choices-panel-cpt":23,"../ui-components/closing-icon-cpt":24,"../ui-components/game-object-cpt":25,"../ui-components/inventory-cpt":26,"../ui-components/inventory-object-cpt":27,"../ui-components/inventory-slot-cpt":28,"../ui-components/scene-animation-cpt":29,"../ui-components/text-box-cpt":30,"../ui-components/user-error-popup":32}],21:[function(require,module,exports){ const supported_locales = ["en", "fr", "es"]; /** @@ -2324,7 +2513,7 @@ module.exports = { get_translated, supported_locales, }; -},{}],21:[function(require,module,exports){ +},{}],22:[function(require,module,exports){ "use strict"; const { get_canvas_font } = require("../lib/font-tools"); @@ -2403,7 +2592,7 @@ class ChoiceCpt extends MtlUiComponent { } module.exports = ChoiceCpt; -},{"../lib/font-tools":6,"../lib/shape-tools":8,"./ui-component":30}],22:[function(require,module,exports){ +},{"../lib/font-tools":7,"../lib/shape-tools":9,"./ui-component":31}],23:[function(require,module,exports){ "use strict"; const MtlUiComponent = require("./ui-component"); @@ -2431,7 +2620,7 @@ class ChoicesPanelCpt extends MtlUiComponent { } module.exports = ChoicesPanelCpt; -},{"./ui-component":30}],23:[function(require,module,exports){ +},{"./ui-component":31}],24:[function(require,module,exports){ "use strict"; const MtlUiComponent = require("./ui-component"); @@ -2483,7 +2672,7 @@ class ClosingIconCpt extends MtlUiComponent { } module.exports = ClosingIconCpt; -},{"./ui-component":30}],24:[function(require,module,exports){ +},{"./ui-component":31}],25:[function(require,module,exports){ "use strict"; const MtlUiComponent = require("./ui-component"); @@ -2525,7 +2714,7 @@ class GameObjectCpt extends MtlUiComponent { } module.exports = GameObjectCpt; -},{"./ui-component":30}],25:[function(require,module,exports){ +},{"./ui-component":31}],26:[function(require,module,exports){ "use strict"; const MtlUiComponent = require("./ui-component"); @@ -2554,7 +2743,7 @@ class InventoryCpt extends MtlUiComponent { } module.exports = InventoryCpt; -},{"./ui-component":30}],26:[function(require,module,exports){ +},{"./ui-component":31}],27:[function(require,module,exports){ "use strict"; const { draw_rect } = require("../lib/shape-tools"); @@ -2604,7 +2793,7 @@ class InventoryObjectCpt extends MtlUiComponent { } module.exports = InventoryObjectCpt; -},{"../lib/shape-tools":8,"./ui-component":30}],27:[function(require,module,exports){ +},{"../lib/shape-tools":9,"./ui-component":31}],28:[function(require,module,exports){ "use strict"; const { draw_rect } = require("../lib/shape-tools"); @@ -2642,7 +2831,7 @@ class InventorySlotCpt extends MtlUiComponent { } module.exports = InventorySlotCpt; -},{"../lib/shape-tools":8,"./ui-component":30}],28:[function(require,module,exports){ +},{"../lib/shape-tools":9,"./ui-component":31}],29:[function(require,module,exports){ "use strict"; const MtlUiComponent = require("./ui-component"); @@ -2738,7 +2927,7 @@ class SceneAnimationCpt extends MtlUiComponent { } module.exports = SceneAnimationCpt; -},{"./ui-component":30}],29:[function(require,module,exports){ +},{"./ui-component":31}],30:[function(require,module,exports){ "use strict"; const { get_canvas_font, get_canvas_char_size } = require("../lib/font-tools"); @@ -2913,7 +3102,7 @@ class TextBoxCpt extends MtlUiComponent { } module.exports = TextBoxCpt; -},{"../lib/font-tools":6,"../lib/shape-tools":8,"./ui-component":30}],30:[function(require,module,exports){ +},{"../lib/font-tools":7,"../lib/shape-tools":9,"./ui-component":31}],31:[function(require,module,exports){ "use strict"; const { draw_rect } = require("../lib/shape-tools"); @@ -3062,7 +3251,7 @@ class MtlUiComponent { } module.exports = MtlUiComponent; -},{"../lib/shape-tools":8}],31:[function(require,module,exports){ +},{"../lib/shape-tools":9}],32:[function(require,module,exports){ "use strict"; const MtlUiComponent = require("./ui-component"); @@ -3114,7 +3303,7 @@ class UserErrorPopup extends MtlUiComponent { } module.exports = UserErrorPopup; -},{"./ui-component":30}],32:[function(require,module,exports){ +},{"./ui-component":31}],33:[function(require,module,exports){ "use strict"; module.exports = { @@ -3331,7 +3520,7 @@ module.exports = { window.dispatchEvent(event); }, }; -},{}],33:[function(require,module,exports){ +},{}],34:[function(require,module,exports){ "use strict"; class ImageCarousel { @@ -3399,7 +3588,7 @@ class ImageCarousel { module.exports = ImageCarousel; -},{}],34:[function(require,module,exports){ +},{}],35:[function(require,module,exports){ "use strict"; const { fetch_json_or_error_text } = require("./fetch"); @@ -3412,8 +3601,8 @@ function getArticleDate(date) { return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`; } -function loadArticles(category) { - return fetch_json_or_error_text(`/articles/${category}`) +function loadArticles(category, locale) { + return fetch_json_or_error_text(`/articles/${category}/${locale}`); } module.exports = { @@ -3422,7 +3611,7 @@ module.exports = { getArticleDate, }; -},{"./fetch":35}],35:[function(require,module,exports){ +},{"./fetch":36}],36:[function(require,module,exports){ "use strict"; function fetchjson(url) { @@ -3461,17 +3650,36 @@ module.exports = { fetch_json_or_error_text, }; -},{}],36:[function(require,module,exports){ +},{}],37:[function(require,module,exports){ "use strict"; +const translator = require("ks-cheap-translator"); +const { translations_url } = require("../../constants"); class WebPage { constructor(args) { Object.assign(this, args); + + if (!this.id) { + this.id = "webpage-" + performance.now(); + } + + translator.init({ + translations_url, + supported_languages: ["fr", "en"], + }).then(this.refresh_all.bind(this)); + } + + refresh() { + obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) + } + + refresh_all() { + obj2htm.renderCycle() } } module.exports = WebPage; -},{}],37:[function(require,module,exports){ +},{"../../constants":3,"ks-cheap-translator":4}],38:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../../../admin-frontend/src/constants"); @@ -3623,6 +3831,7 @@ class GameArticle { { tag: "label", contents: detail.label }, { tag: "div", + class: "detail-value", contents: detail.value }, ], @@ -3638,11 +3847,12 @@ class GameArticle { module.exports = GameArticle; -},{"../../../../../admin-frontend/src/constants":1,"../../../../constants":3,"../../../generic-components/image-carousel":33,"../../../lib/article-utils":34,"../../../lib/fetch":35,"mentalo-engine":4}],38:[function(require,module,exports){ +},{"../../../../../admin-frontend/src/constants":1,"../../../../constants":3,"../../../generic-components/image-carousel":34,"../../../lib/article-utils":35,"../../../lib/fetch":36,"mentalo-engine":5}],39:[function(require,module,exports){ "use strict"; const { loadArticles } = require("../../../lib/article-utils"); const GameArticle = require("./game-article"); +const translator = require("ks-cheap-translator"); class GameArticles { constructor(props) { @@ -3650,12 +3860,12 @@ class GameArticles { this.state = { articles: [], }; - this.id = performance.now(); + this.id = "game-articles-section"; this.loadArticles(); } loadArticles() { - loadArticles("games") + loadArticles("games", translator.locale) .then(articles => { this.state.articles = articles; this.refresh(); @@ -3693,12 +3903,14 @@ class GameArticles { module.exports = GameArticles; -},{"../../../lib/article-utils":34,"./game-article":37}],39:[function(require,module,exports){ +},{"../../../lib/article-utils":35,"./game-article":38,"ks-cheap-translator":4}],40:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../constants"); const WebPage = require("../../lib/web-page"); const GameArticles = require("./components/game-articles"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); class GamesPage extends WebPage { render() { @@ -3725,11 +3937,10 @@ class GamesPage extends WebPage { }, ], }, - { tag: "h1", contents: "Jeux" }, + { tag: "h1", contents: t("Jeux") }, { tag: "p", - contents: `Création de jeux vidéos indépendants. - <br/>Jeux web, PC et projets en cours de développement`, + contents: t("games-page-intro"), }, ], }, @@ -3743,7 +3954,7 @@ class GamesPage extends WebPage { module.exports = GamesPage; -},{"../../../constants":3,"../../lib/web-page":36,"./components/game-articles":38}],40:[function(require,module,exports){ +},{"../../../constants":3,"../../lib/web-page":37,"./components/game-articles":39,"ks-cheap-translator":4}],41:[function(require,module,exports){ "use strict"; "use strict"; @@ -3751,7 +3962,7 @@ const runPage = require("../../run-page"); const GamesPage = require("./games"); runPage(GamesPage); -},{"../../run-page":41,"./games":39}],41:[function(require,module,exports){ +},{"../../run-page":42,"./games":40}],42:[function(require,module,exports){ "use strict"; const renderer = require("object-to-html-renderer") @@ -3764,19 +3975,18 @@ module.exports = function runPage(PageComponent) { obj2htm.renderCycle(); }; -},{"./template/template":43,"object-to-html-renderer":32}],42:[function(require,module,exports){ +},{"./template/template":44,"object-to-html-renderer":33}],43:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../constants"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); const NAV_MENU_ITEMS = [ { url: "/games/", text: "Jeux" }, { url: "/education/", text: "Pédagogie", - // submenu: [ - // { url: "/gamedev", text: "Création de jeux vidéo" }, - // ] }, { url: "/software-development/", text: "Software" } ]; @@ -3802,6 +4012,12 @@ class NavBar { }); } + handle_chang_lang(lang) { + translator.update_translations(lang).then(() => { + obj2htm.renderCycle(); + }).catch(err => console.log(err)); + } + renderHome() { return { tag: "div", @@ -3843,10 +4059,20 @@ class NavBar { { tag: "a", href, - contents: text, + contents: t(text), }, ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), }; + }).concat({ + tag: "li", + class: "lang-flags", + contents: ["fr", "en"].map(lang => { + return { + tag: "img", src: `${images_url}/flag-${lang}.svg`, + class: translator.locale === lang ? "selected" : "", + onclick: this.handle_chang_lang.bind(this, lang) + } + }) }), }; } @@ -3874,12 +4100,14 @@ class NavBar { module.exports = NavBar; -},{"../../../constants":3}],43:[function(require,module,exports){ +},{"../../../constants":3,"ks-cheap-translator":4}],44:[function(require,module,exports){ "use strict"; const { in_construction } = require("../../config"); const { images_url } = require("../../constants"); const NavBar = require("./components/navbar"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator) class Template { constructor(props) { @@ -3900,7 +4128,7 @@ class Template { { tag: "strong", class: "page-contents-center", - contents: "Site en construction ...", + contents: t("Site en construction ..."), }, ], }, @@ -3950,7 +4178,7 @@ class Template { contents: [ { tag: "strong", - contents: "<blue>Sur les réseaux : </blue>", + contents: `<blue>${t("Sur les réseaux")} : </blue>`, }, { tag: "a", @@ -3972,11 +4200,11 @@ class Template { tag: "span", contents: `Copyleft 🄯 ${new Date() .getFullYear()} Kuadrado Software | - Toutes les images du site ont été réalisées par mes soins et peuvent être réutilisées pour un usage personnel.`, + ${t("kuadrado-footer-copyleft")}`, }, { tag: "div", contents: [ - { tag: "span", contents: "Ce site web est " }, + { tag: "span", contents: t("Ce site web est") + " " }, { tag: "a", target: "_blank", style_rules: { fontWeight: "bold" }, @@ -3994,4 +4222,4 @@ class Template { module.exports = Template; -},{"../../config":2,"../../constants":3,"./components/navbar":42}]},{},[40]); +},{"../../config":2,"../../constants":3,"./components/navbar":43,"ks-cheap-translator":4}]},{},[41]); diff --git a/public/main.js b/public/main.js index 7fd0ea16e95a3637ebe5ca2e0f68eabc403fd94f..1e3f2ac561a68051d057a398bb632213e26b0cca 100644 --- a/public/main.js +++ b/public/main.js @@ -10,10 +10,199 @@ module.exports = { module.exports = { images_url: `/assets/images`, data_url: `/assets/data`, + translations_url: "/assets/translations" }; },{}],3:[function(require,module,exports){ "use strict"; +/** + * A tiny library to handle text translation. + */ + +/** + * init parameter object type + * @typedef TranslatorParam + * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes + * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en" + * + * @property {Boolean} use_url_locale_fragment - DEFAULT: true - + * If true, the 1st fragment of the window.location url will be parsed as a language code + * Example: + * window.location + * > https://example.com/en/some-page... + * then the /en/ part of the url will be taken in priority and locale will be set to "en". + * window.location + * > https://example.com/some-page... + * No locale fragment will be found so locale will not be set from url fragment. + * window.location + * > https://example.com/some-page/en + * Doesn't work, locale fragment must be the first framgment + * + * @property {String} local_storage_key - DEFAULT: "translator-prefered-language" + * The key used to saved the current locale into local storage + * + * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations + * Translations files are expected to be named with their corresponding locale code. + * Example: + * if supported_languages is set to ["en", "fr", "it"] + * and translations_url is set to "https://example.com/translations/"" + * Then the expected translations files are + * https://example.com/translations/en.json + * https://example.com/translations/fr.json + * https://example.com/translations/it.json + * The json resources must simple key value maps, value being the translated text. + */ + +module.exports = { + locale: "en", // ISO 639 lowercase language code + supported_languages: ["en"], + translations: {}, + translations_url: "", + use_url_locale_fragment: true, + local_storage_key: "translator-prefered-language", + + /** + * Initialize the lib with params + * @param {TranslatorParam} params + * @returns {Promise} + */ + init(params) { + Object.entries(params).forEach(k_v => { + const [key, value] = k_v; + if ([ + "supported_languages", + "use_url_locale_fragment", + "local_storage_key", + "translations_url" + ].includes(key)) { + this[key] = value; + } + }); + + this.translations_url = this.format_translations_url(this.translations_url); + this.supported_languages = this.format_supported_languages(this.supported_languages); + + return new Promise((resolve, reject) => { + const loc = + (() => {// Locale from url priority 1 + if (this.use_url_locale_fragment) { + const first_url_fragment = window.location.pathname.substring(1).split("/")[0]; + const fragment_is_locale = this.supported_languages.includes(first_url_fragment); + return fragment_is_locale ? first_url_fragment : ""; + } else { + return ""; + } + })() + || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2 + || (() => { // locale from navigator priority 3 + const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase(); + return this.supported_languages.includes(navigator_locale) + ? navigator_locale + : this.supported_languages[0] // Default if navigator locale is not supported + })(); + + fetch(`${this.translations_url}${loc}.json`) + .then(response => response.json()) + .then(response => { + this.locale = loc; + this.translations = response; + resolve(); + }) + .catch(err => { + this.locale = "en"; + reject(err); + }); + }); + }, + + /** + * Return a lowercase string without dash. + * If given locale is en-EN, then "en" will be returned. + * @param {String} locale + * @returns A lowercase string + */ + format_locale(locale) { + return locale.split("-")[0].toLowerCase(); + }, + + /** + * Appends a slash at the end of the given string if missing, and returns the url. + * @param {String} url + * @returns {String} + */ + format_translations_url(url) { + if (url.charAt(url.length - 1) !== "/") { + url += "/" + } + return url; + }, + + /** + * Return the array of language codes formatted as lowsercase language code. + * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned. + * @param {String[]} languages_codes + * @returns {String[]} + */ + format_supported_languages(languages_codes) { + return languages_codes.map(lc => this.format_locale(lc)); + }, + + /** + * Fetches a new set of translations in case the wanted language changes + * @param {String} locale A lowercase language code + * @returns {Promise} + */ + update_translations(locale) { + locale = this.format_locale(locale); + return new Promise((resolve, reject) => { + fetch(`${this.translations_url}${locale}.json`) + .then(response => response.json()) + .then(response => { + this.translations = response; + this.locale = locale; + + localStorage.setItem(this.local_storage_key, locale); + + const split_path = window.location.pathname.substring(1).split("/"); + const first_url_fragment = split_path[0]; + const fragment_is_locale = this.supported_languages.includes(first_url_fragment); + + if (fragment_is_locale) { + split_path.splice(0, 1, locale); + const updated_path = split_path.join("/"); + window.history.replaceState(null, "", "/" + updated_path); + } + resolve(); + }) + .catch(err => { + reject(err); + }); + }); + }, + + /** + * Tries to get the translation of the source string, or return the string as it. + * @param {String} text The source text to translate + * @param {Object} params Some dynamic element to insert in the text + * Params can be used if the translated text provide placeholder like {%some_word%} + * Example: + * translator.trad("Some trad key", {some_word: "a dynamic parameter"}) + * -> translation for "Some trad key": "A translated text with {%some_word%}" + * -> will be return as : "A translated text with a dynamic parameter" + * @returns {String} + */ + trad: function (text, params = {}) { + text = this.translations[text] || text; + + Object.keys(params).forEach(k => { + text = text.replace(`{%${k}%}`, params[k]); + }); + + return text; + } +}; +},{}],4:[function(require,module,exports){ +"use strict"; module.exports = { register_key: "objectToHtmlRender", @@ -229,7 +418,7 @@ module.exports = { window.dispatchEvent(event); }, }; -},{}],4:[function(require,module,exports){ +},{}],5:[function(require,module,exports){ "use strict"; const { images_url } = require("../../constants"); @@ -267,18 +456,24 @@ class ThemeCard { module.exports = ThemeCard; -},{"../../constants":2}],5:[function(require,module,exports){ +},{"../../constants":2}],6:[function(require,module,exports){ "use strict"; const { images_url } = require("../constants"); const ThemeCard = require("./home-page-components/theme-card"); const WebPage = require("./lib/web-page"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); class HomePage extends WebPage { + constructor() { + super({ id: "home-page" }); + } + render() { return { tag: "div", - id: "home-page", + id: this.id, contents: [ { tag: "div", @@ -305,12 +500,12 @@ class HomePage extends WebPage { { tag: "p", class: "page-contents-center", - contents: `Studio de création de jeux vidéo basé en Ardèche, Vernoux en Vivarais.<br />Création artisitique numérique | Développement d'outillage logiciel libre et open source | Pédagogie.`, + contents: t("kuadrado-home-description"), }, { tag: "ul", class: "philo-bubbles", - contents: ["Simplicité", "Légèreté", "Écologie"].map(word => { + contents: [t("Simplicité"), t("Légèreté"), t("Écologie")].map(word => { return { tag: "li", contents: [{ tag: "span", contents: word }], @@ -324,23 +519,23 @@ class HomePage extends WebPage { class: "page-contents-center poles", contents: [ { - title: "Jeux", + title: t("Jeux"), img: "game_controller.svg", href: "/games/", description: - "Créations vidéoludiques, jeux web et jeux PC, projets en cours.", + t("games-description"), }, { - title: "Pédagogie", + title: t("Pédagogie"), img: "brain.svg", href: "/education/", - description: `S'approprier la technologie par le partage de connaissances.`, + description: t("education-description"), }, { title: "Software", img: "meca_proc.svg", href: "/software-development/", - description: `R&D, projets expérimentaux, web et outillage logiciel`, + description: t("software-description"), }, ].map(cardProps => new ThemeCard(cardProps).render()), }, @@ -351,17 +546,36 @@ class HomePage extends WebPage { module.exports = HomePage; -},{"../constants":2,"./home-page-components/theme-card":4,"./lib/web-page":6}],6:[function(require,module,exports){ +},{"../constants":2,"./home-page-components/theme-card":5,"./lib/web-page":7,"ks-cheap-translator":3}],7:[function(require,module,exports){ "use strict"; +const translator = require("ks-cheap-translator"); +const { translations_url } = require("../../constants"); class WebPage { constructor(args) { Object.assign(this, args); + + if (!this.id) { + this.id = "webpage-" + performance.now(); + } + + translator.init({ + translations_url, + supported_languages: ["fr", "en"], + }).then(this.refresh_all.bind(this)); + } + + refresh() { + obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) + } + + refresh_all() { + obj2htm.renderCycle() } } module.exports = WebPage; -},{}],7:[function(require,module,exports){ +},{"../../constants":2,"ks-cheap-translator":3}],8:[function(require,module,exports){ "use strict"; const HomePage = require("./homepage"); @@ -369,7 +583,7 @@ const runPage = require("./run-page"); runPage(HomePage); -},{"./homepage":5,"./run-page":8}],8:[function(require,module,exports){ +},{"./homepage":6,"./run-page":9}],9:[function(require,module,exports){ "use strict"; const renderer = require("object-to-html-renderer") @@ -382,19 +596,18 @@ module.exports = function runPage(PageComponent) { obj2htm.renderCycle(); }; -},{"./template/template":10,"object-to-html-renderer":3}],9:[function(require,module,exports){ +},{"./template/template":11,"object-to-html-renderer":4}],10:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../constants"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); const NAV_MENU_ITEMS = [ { url: "/games/", text: "Jeux" }, { url: "/education/", text: "Pédagogie", - // submenu: [ - // { url: "/gamedev", text: "Création de jeux vidéo" }, - // ] }, { url: "/software-development/", text: "Software" } ]; @@ -420,6 +633,12 @@ class NavBar { }); } + handle_chang_lang(lang) { + translator.update_translations(lang).then(() => { + obj2htm.renderCycle(); + }).catch(err => console.log(err)); + } + renderHome() { return { tag: "div", @@ -461,10 +680,20 @@ class NavBar { { tag: "a", href, - contents: text, + contents: t(text), }, ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), }; + }).concat({ + tag: "li", + class: "lang-flags", + contents: ["fr", "en"].map(lang => { + return { + tag: "img", src: `${images_url}/flag-${lang}.svg`, + class: translator.locale === lang ? "selected" : "", + onclick: this.handle_chang_lang.bind(this, lang) + } + }) }), }; } @@ -492,12 +721,14 @@ class NavBar { module.exports = NavBar; -},{"../../../constants":2}],10:[function(require,module,exports){ +},{"../../../constants":2,"ks-cheap-translator":3}],11:[function(require,module,exports){ "use strict"; const { in_construction } = require("../../config"); const { images_url } = require("../../constants"); const NavBar = require("./components/navbar"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator) class Template { constructor(props) { @@ -518,7 +749,7 @@ class Template { { tag: "strong", class: "page-contents-center", - contents: "Site en construction ...", + contents: t("Site en construction ..."), }, ], }, @@ -568,7 +799,7 @@ class Template { contents: [ { tag: "strong", - contents: "<blue>Sur les réseaux : </blue>", + contents: `<blue>${t("Sur les réseaux")} : </blue>`, }, { tag: "a", @@ -590,11 +821,11 @@ class Template { tag: "span", contents: `Copyleft 🄯 ${new Date() .getFullYear()} Kuadrado Software | - Toutes les images du site ont été réalisées par mes soins et peuvent être réutilisées pour un usage personnel.`, + ${t("kuadrado-footer-copyleft")}`, }, { tag: "div", contents: [ - { tag: "span", contents: "Ce site web est " }, + { tag: "span", contents: t("Ce site web est") + " " }, { tag: "a", target: "_blank", style_rules: { fontWeight: "bold" }, @@ -612,4 +843,4 @@ class Template { module.exports = Template; -},{"../../config":1,"../../constants":2,"./components/navbar":9}]},{},[7]); +},{"../../config":1,"../../constants":2,"./components/navbar":10,"ks-cheap-translator":3}]},{},[8]); diff --git a/public/software-development/software-development.js b/public/software-development/software-development.js index 76ff0b825f09ca3cde10efda75d1952fdaf9228b..c2ed85689dc4575b4264884d7be01b338cdbbeba 100644 --- a/public/software-development/software-development.js +++ b/public/software-development/software-development.js @@ -14,10 +14,199 @@ module.exports = { module.exports = { images_url: `/assets/images`, data_url: `/assets/data`, + translations_url: "/assets/translations" }; },{}],4:[function(require,module,exports){ "use strict"; +/** + * A tiny library to handle text translation. + */ + +/** + * init parameter object type + * @typedef TranslatorParam + * @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes + * @property {String} locale ISO 639 lowercase language code - DEFAULT: "en" + * + * @property {Boolean} use_url_locale_fragment - DEFAULT: true - + * If true, the 1st fragment of the window.location url will be parsed as a language code + * Example: + * window.location + * > https://example.com/en/some-page... + * then the /en/ part of the url will be taken in priority and locale will be set to "en". + * window.location + * > https://example.com/some-page... + * No locale fragment will be found so locale will not be set from url fragment. + * window.location + * > https://example.com/some-page/en + * Doesn't work, locale fragment must be the first framgment + * + * @property {String} local_storage_key - DEFAULT: "translator-prefered-language" + * The key used to saved the current locale into local storage + * + * @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations + * Translations files are expected to be named with their corresponding locale code. + * Example: + * if supported_languages is set to ["en", "fr", "it"] + * and translations_url is set to "https://example.com/translations/"" + * Then the expected translations files are + * https://example.com/translations/en.json + * https://example.com/translations/fr.json + * https://example.com/translations/it.json + * The json resources must simple key value maps, value being the translated text. + */ + +module.exports = { + locale: "en", // ISO 639 lowercase language code + supported_languages: ["en"], + translations: {}, + translations_url: "", + use_url_locale_fragment: true, + local_storage_key: "translator-prefered-language", + + /** + * Initialize the lib with params + * @param {TranslatorParam} params + * @returns {Promise} + */ + init(params) { + Object.entries(params).forEach(k_v => { + const [key, value] = k_v; + if ([ + "supported_languages", + "use_url_locale_fragment", + "local_storage_key", + "translations_url" + ].includes(key)) { + this[key] = value; + } + }); + + this.translations_url = this.format_translations_url(this.translations_url); + this.supported_languages = this.format_supported_languages(this.supported_languages); + + return new Promise((resolve, reject) => { + const loc = + (() => {// Locale from url priority 1 + if (this.use_url_locale_fragment) { + const first_url_fragment = window.location.pathname.substring(1).split("/")[0]; + const fragment_is_locale = this.supported_languages.includes(first_url_fragment); + return fragment_is_locale ? first_url_fragment : ""; + } else { + return ""; + } + })() + || localStorage.getItem(this.local_storage_key) // Locale from storage priority 2 + || (() => { // locale from navigator priority 3 + const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase(); + return this.supported_languages.includes(navigator_locale) + ? navigator_locale + : this.supported_languages[0] // Default if navigator locale is not supported + })(); + + fetch(`${this.translations_url}${loc}.json`) + .then(response => response.json()) + .then(response => { + this.locale = loc; + this.translations = response; + resolve(); + }) + .catch(err => { + this.locale = "en"; + reject(err); + }); + }); + }, + + /** + * Return a lowercase string without dash. + * If given locale is en-EN, then "en" will be returned. + * @param {String} locale + * @returns A lowercase string + */ + format_locale(locale) { + return locale.split("-")[0].toLowerCase(); + }, + + /** + * Appends a slash at the end of the given string if missing, and returns the url. + * @param {String} url + * @returns {String} + */ + format_translations_url(url) { + if (url.charAt(url.length - 1) !== "/") { + url += "/" + } + return url; + }, + + /** + * Return the array of language codes formatted as lowsercase language code. + * if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned. + * @param {String[]} languages_codes + * @returns {String[]} + */ + format_supported_languages(languages_codes) { + return languages_codes.map(lc => this.format_locale(lc)); + }, + + /** + * Fetches a new set of translations in case the wanted language changes + * @param {String} locale A lowercase language code + * @returns {Promise} + */ + update_translations(locale) { + locale = this.format_locale(locale); + return new Promise((resolve, reject) => { + fetch(`${this.translations_url}${locale}.json`) + .then(response => response.json()) + .then(response => { + this.translations = response; + this.locale = locale; + + localStorage.setItem(this.local_storage_key, locale); + + const split_path = window.location.pathname.substring(1).split("/"); + const first_url_fragment = split_path[0]; + const fragment_is_locale = this.supported_languages.includes(first_url_fragment); + + if (fragment_is_locale) { + split_path.splice(0, 1, locale); + const updated_path = split_path.join("/"); + window.history.replaceState(null, "", "/" + updated_path); + } + resolve(); + }) + .catch(err => { + reject(err); + }); + }); + }, + + /** + * Tries to get the translation of the source string, or return the string as it. + * @param {String} text The source text to translate + * @param {Object} params Some dynamic element to insert in the text + * Params can be used if the translated text provide placeholder like {%some_word%} + * Example: + * translator.trad("Some trad key", {some_word: "a dynamic parameter"}) + * -> translation for "Some trad key": "A translated text with {%some_word%}" + * -> will be return as : "A translated text with a dynamic parameter" + * @returns {String} + */ + trad: function (text, params = {}) { + text = this.translations[text] || text; + + Object.keys(params).forEach(k => { + text = text.replace(`{%${k}%}`, params[k]); + }); + + return text; + } +}; +},{}],5:[function(require,module,exports){ +"use strict"; module.exports = { register_key: "objectToHtmlRender", @@ -233,7 +422,7 @@ module.exports = { window.dispatchEvent(event); }, }; -},{}],5:[function(require,module,exports){ +},{}],6:[function(require,module,exports){ "use strict"; const { fetch_json_or_error_text } = require("./fetch"); @@ -246,8 +435,8 @@ function getArticleDate(date) { return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`; } -function loadArticles(category) { - return fetch_json_or_error_text(`/articles/${category}`) +function loadArticles(category, locale) { + return fetch_json_or_error_text(`/articles/${category}/${locale}`); } module.exports = { @@ -256,7 +445,7 @@ module.exports = { getArticleDate, }; -},{"./fetch":6}],6:[function(require,module,exports){ +},{"./fetch":7}],7:[function(require,module,exports){ "use strict"; function fetchjson(url) { @@ -295,17 +484,36 @@ module.exports = { fetch_json_or_error_text, }; -},{}],7:[function(require,module,exports){ +},{}],8:[function(require,module,exports){ "use strict"; +const translator = require("ks-cheap-translator"); +const { translations_url } = require("../../constants"); class WebPage { constructor(args) { Object.assign(this, args); + + if (!this.id) { + this.id = "webpage-" + performance.now(); + } + + translator.init({ + translations_url, + supported_languages: ["fr", "en"], + }).then(this.refresh_all.bind(this)); + } + + refresh() { + obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) + } + + refresh_all() { + obj2htm.renderCycle() } } module.exports = WebPage; -},{}],8:[function(require,module,exports){ +},{"../../constants":3,"ks-cheap-translator":4}],9:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../../../admin-frontend/src/constants"); @@ -370,6 +578,7 @@ class SoftwareArticle { { tag: "label", contents: detail.label }, { tag: "div", + class: "detail-value", contents: detail.value }, ], @@ -384,11 +593,12 @@ class SoftwareArticle { } module.exports = SoftwareArticle; -},{"../../../../../admin-frontend/src/constants":1,"../../../lib/article-utils":5}],9:[function(require,module,exports){ +},{"../../../../../admin-frontend/src/constants":1,"../../../lib/article-utils":6}],10:[function(require,module,exports){ "use strict"; const { loadArticles } = require("../../../lib/article-utils"); const SoftwareArticle = require("./software-article"); +const translator = require("ks-cheap-translator"); class SoftwareArticles { constructor(props) { @@ -396,12 +606,12 @@ class SoftwareArticles { this.state = { articles: [], }; - this.id = performance.now(); + this.id = "software-articles-section"; this.loadArticles(); } loadArticles() { - loadArticles("software").then(articles => { + loadArticles("software", translator.locale).then(articles => { this.state.articles = articles; this.refresh(); this.fixScroll(); @@ -452,12 +662,14 @@ class SoftwareArticles { module.exports = SoftwareArticles; -},{"../../../lib/article-utils":5,"./software-article":8}],10:[function(require,module,exports){ +},{"../../../lib/article-utils":6,"./software-article":9,"ks-cheap-translator":4}],11:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../constants"); const WebPage = require("../../lib/web-page"); const SoftwareArticles = require("./components/software-articles"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); class SoftwareDevelopment extends WebPage { render() { @@ -487,7 +699,7 @@ class SoftwareDevelopment extends WebPage { { tag: "h1", contents: "Software" }, { tag: "p", - contents: `R&D, projets expérimentaux, outillage logiciel pour le développement de jeu ou pour le web.`, + contents: t("software-page-intro"), }, ], }, @@ -501,7 +713,7 @@ class SoftwareDevelopment extends WebPage { module.exports = SoftwareDevelopment; -},{"../../../constants":3,"../../lib/web-page":7,"./components/software-articles":9}],11:[function(require,module,exports){ +},{"../../../constants":3,"../../lib/web-page":8,"./components/software-articles":10,"ks-cheap-translator":4}],12:[function(require,module,exports){ "use strict"; "use strict"; @@ -509,7 +721,7 @@ const runPage = require("../../run-page"); const SoftwareDevelopment = require("./software-development"); runPage(SoftwareDevelopment); -},{"../../run-page":12,"./software-development":10}],12:[function(require,module,exports){ +},{"../../run-page":13,"./software-development":11}],13:[function(require,module,exports){ "use strict"; const renderer = require("object-to-html-renderer") @@ -522,19 +734,18 @@ module.exports = function runPage(PageComponent) { obj2htm.renderCycle(); }; -},{"./template/template":14,"object-to-html-renderer":4}],13:[function(require,module,exports){ +},{"./template/template":15,"object-to-html-renderer":5}],14:[function(require,module,exports){ "use strict"; const { images_url } = require("../../../constants"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); const NAV_MENU_ITEMS = [ { url: "/games/", text: "Jeux" }, { url: "/education/", text: "Pédagogie", - // submenu: [ - // { url: "/gamedev", text: "Création de jeux vidéo" }, - // ] }, { url: "/software-development/", text: "Software" } ]; @@ -560,6 +771,12 @@ class NavBar { }); } + handle_chang_lang(lang) { + translator.update_translations(lang).then(() => { + obj2htm.renderCycle(); + }).catch(err => console.log(err)); + } + renderHome() { return { tag: "div", @@ -601,10 +818,20 @@ class NavBar { { tag: "a", href, - contents: text, + contents: t(text), }, ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), }; + }).concat({ + tag: "li", + class: "lang-flags", + contents: ["fr", "en"].map(lang => { + return { + tag: "img", src: `${images_url}/flag-${lang}.svg`, + class: translator.locale === lang ? "selected" : "", + onclick: this.handle_chang_lang.bind(this, lang) + } + }) }), }; } @@ -632,12 +859,14 @@ class NavBar { module.exports = NavBar; -},{"../../../constants":3}],14:[function(require,module,exports){ +},{"../../../constants":3,"ks-cheap-translator":4}],15:[function(require,module,exports){ "use strict"; const { in_construction } = require("../../config"); const { images_url } = require("../../constants"); const NavBar = require("./components/navbar"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator) class Template { constructor(props) { @@ -658,7 +887,7 @@ class Template { { tag: "strong", class: "page-contents-center", - contents: "Site en construction ...", + contents: t("Site en construction ..."), }, ], }, @@ -708,7 +937,7 @@ class Template { contents: [ { tag: "strong", - contents: "<blue>Sur les réseaux : </blue>", + contents: `<blue>${t("Sur les réseaux")} : </blue>`, }, { tag: "a", @@ -730,11 +959,11 @@ class Template { tag: "span", contents: `Copyleft 🄯 ${new Date() .getFullYear()} Kuadrado Software | - Toutes les images du site ont été réalisées par mes soins et peuvent être réutilisées pour un usage personnel.`, + ${t("kuadrado-footer-copyleft")}`, }, { tag: "div", contents: [ - { tag: "span", contents: "Ce site web est " }, + { tag: "span", contents: t("Ce site web est") + " " }, { tag: "a", target: "_blank", style_rules: { fontWeight: "bold" }, @@ -752,4 +981,4 @@ class Template { module.exports = Template; -},{"../../config":2,"../../constants":3,"./components/navbar":13}]},{},[11]); +},{"../../config":2,"../../constants":3,"./components/navbar":14,"ks-cheap-translator":4}]},{},[12]); diff --git a/public/style/style.css b/public/style/style.css index 3a5bebbd60c34e95366d34385e1915791bad3e04..db7b101962d4bf8d3e1fd5c1228b671f0fea983d 100644 --- a/public/style/style.css +++ b/public/style/style.css @@ -146,6 +146,7 @@ main header nav ul { margin: 0; list-style-type: none; height: 100%; + flex: 1; } main header nav ul li { position: relative; @@ -190,6 +191,22 @@ main header nav ul li:hover .submenu a { main header nav ul li:hover .submenu li:hover a { color: #3c4144; } +main header nav ul li.lang-flags { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; + padding: 0 20px; +} +main header nav ul li.lang-flags img { + width: 35px; + height: 30px; + cursor: pointer; + opacity: 0.5; +} +main header nav ul li.lang-flags img.selected, main header nav ul li.lang-flags img:hover { + opacity: 1; +} main header nav .burger { display: none; } @@ -247,6 +264,11 @@ main header nav .burger { font-size: 14px; color: #96a5ae; } + main header nav ul.responsive-show li.lang-flags { + margin-left: unset; + justify-content: space-around; + padding: 20px; + } } main #page-container { width: 100%; @@ -397,6 +419,9 @@ main #page-container .article-details ul.details-list .detail label { font-weight: bold; color: #6b7880; } +main #page-container .article-details ul.details-list .detail .detail-value { + text-align: right; +} main #page-container #home-page { display: flex; flex-direction: column; diff --git a/src/service/articles.rs b/src/service/articles.rs index 692ca3c5480ab452fa1e15c69b8a066db78650cc..13219d6e1ef5e7a4f9886513634ae6ce23f46ad5 100644 --- a/src/service/articles.rs +++ b/src/service/articles.rs @@ -107,13 +107,15 @@ pub async fn delete_article( } } -#[get("/articles/{category}")] +#[get("/articles/{category}/{locale}")] pub async fn get_articles_by_category( app_state: Data<AppState>, - category: Path<String>, + path: Path<(String, String)>, ) -> impl Responder { + let (category, locale) = path.into_inner(); + match get_collection(&app_state) - .find(doc! {"category": category.into_inner()}, None) + .find(doc! {"category": category, "locale": locale}, None) .await { Ok(mut cursor) => { @@ -610,7 +612,7 @@ mod test_articles { .await .unwrap(); - let req = test::TestRequest::with_uri("/articles/testing") + let req = test::TestRequest::with_uri("/articles/testing/fr") .header("Accept", "application/json") .method(Method::GET) .to_request(); diff --git a/website/constants.js b/website/constants.js index bddb27a4d9bc2e3961443ee7a05a6e3b75ef5d7a..6fd1333287c8b8472dad3cdda10ff05d942d5254 100644 --- a/website/constants.js +++ b/website/constants.js @@ -1,4 +1,5 @@ module.exports = { images_url: `/assets/images`, data_url: `/assets/data`, + translations_url: "/assets/translations" }; diff --git a/website/package-lock.json b/website/package-lock.json index e343bea44b26ab0b6705fc1332b8efa83f2b7229..161971b07ea62e2b31cff5b7be5ef99f159749c3 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.3", "license": "MIT", "dependencies": { + "ks-cheap-translator": "^0.1.0", "mentalo-engine": "0.1.17", "object-to-html-renderer": "^1.1.1" }, @@ -1139,6 +1140,11 @@ "node": "*" } }, + "node_modules/ks-cheap-translator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ks-cheap-translator/-/ks-cheap-translator-0.1.0.tgz", + "integrity": "sha512-h9ymFx7Z36M4Te4jOLlqgIkhHhUnNCEw2X1LCQY10c9mC7X3EL33OLj/bwKlu8a5SyWy+1DnuGbq5//rJuHEyg==" + }, "node_modules/labeled-stream-splicer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", @@ -2908,6 +2914,11 @@ "through": ">=2.2.7 <3" } }, + "ks-cheap-translator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ks-cheap-translator/-/ks-cheap-translator-0.1.0.tgz", + "integrity": "sha512-h9ymFx7Z36M4Te4jOLlqgIkhHhUnNCEw2X1LCQY10c9mC7X3EL33OLj/bwKlu8a5SyWy+1DnuGbq5//rJuHEyg==" + }, "labeled-stream-splicer": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", diff --git a/website/package.json b/website/package.json index 2260a39d6f72b2bfee8b751fa293427bc9cbeaf7..9be127e3029ffc7ddeef1ac8acd184489a4b020b 100644 --- a/website/package.json +++ b/website/package.json @@ -14,11 +14,12 @@ "license": "MIT", "homepage": "https://gitlab.com/peter_rabbit/kuadrado-website#readme", "dependencies": { - "object-to-html-renderer": "^1.1.1", - "mentalo-engine": "0.1.17" + "ks-cheap-translator": "^0.1.0", + "mentalo-engine": "0.1.17", + "object-to-html-renderer": "^1.1.1" }, "devDependencies": { "sass": "^1.32.0", "simple-browser-js-bundler": "^0.1.1" } -} \ No newline at end of file +} diff --git a/website/src/homepage.js b/website/src/homepage.js index 59c22b12c21d90492d93239699580ffdf3723e84..fc03455a6459c93081dd6ac05451ed7dd4bac995 100644 --- a/website/src/homepage.js +++ b/website/src/homepage.js @@ -3,12 +3,18 @@ const { images_url } = require("../constants"); const ThemeCard = require("./home-page-components/theme-card"); const WebPage = require("./lib/web-page"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); class HomePage extends WebPage { + constructor() { + super({ id: "home-page" }); + } + render() { return { tag: "div", - id: "home-page", + id: this.id, contents: [ { tag: "div", @@ -35,12 +41,12 @@ class HomePage extends WebPage { { tag: "p", class: "page-contents-center", - contents: `Studio de création de jeux vidéo basé en Ardèche, Vernoux en Vivarais.<br />Création artisitique numérique | Développement d'outillage logiciel libre et open source | Pédagogie.`, + contents: t("kuadrado-home-description"), }, { tag: "ul", class: "philo-bubbles", - contents: ["Simplicité", "Légèreté", "Écologie"].map(word => { + contents: [t("Simplicité"), t("Légèreté"), t("Écologie")].map(word => { return { tag: "li", contents: [{ tag: "span", contents: word }], @@ -54,23 +60,23 @@ class HomePage extends WebPage { class: "page-contents-center poles", contents: [ { - title: "Jeux", + title: t("Jeux"), img: "game_controller.svg", href: "/games/", description: - "Créations vidéoludiques, jeux web et jeux PC, projets en cours.", + t("games-description"), }, { - title: "Pédagogie", + title: t("Pédagogie"), img: "brain.svg", href: "/education/", - description: `S'approprier la technologie par le partage de connaissances.`, + description: t("education-description"), }, { title: "Software", img: "meca_proc.svg", href: "/software-development/", - description: `R&D, projets expérimentaux, web et outillage logiciel`, + description: t("software-description"), }, ].map(cardProps => new ThemeCard(cardProps).render()), }, diff --git a/website/src/lib/article-utils.js b/website/src/lib/article-utils.js index 3e00a88258938157bad464a9949e59a624e674b6..fbce5d4096d62bbe9affe2d6a2e90a935369b109 100644 --- a/website/src/lib/article-utils.js +++ b/website/src/lib/article-utils.js @@ -10,8 +10,8 @@ function getArticleDate(date) { return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`; } -function loadArticles(category) { - return fetch_json_or_error_text(`/articles/${category}`) +function loadArticles(category, locale) { + return fetch_json_or_error_text(`/articles/${category}/${locale}`); } module.exports = { diff --git a/website/src/lib/web-page.js b/website/src/lib/web-page.js index b58f4c0a88f85c2b45ca6220c6ed4651488dbfb1..39631cb3dd4f6c9ea1a0b0fc541ff05b13d02dcb 100644 --- a/website/src/lib/web-page.js +++ b/website/src/lib/web-page.js @@ -1,8 +1,27 @@ "use strict"; +const translator = require("ks-cheap-translator"); +const { translations_url } = require("../../constants"); class WebPage { constructor(args) { Object.assign(this, args); + + if (!this.id) { + this.id = "webpage-" + performance.now(); + } + + translator.init({ + translations_url, + supported_languages: ["fr", "en"], + }).then(this.refresh_all.bind(this)); + } + + refresh() { + obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" }) + } + + refresh_all() { + obj2htm.renderCycle() } } diff --git a/website/src/pages/education/education.js b/website/src/pages/education/education.js index 146cd3bf206eeb5edf6e04606f981fc4f96964be..efd67647628720b8b56333c6fda7f27620280728 100644 --- a/website/src/pages/education/education.js +++ b/website/src/pages/education/education.js @@ -2,55 +2,38 @@ const { images_url } = require("../../../constants"); const WebPage = require("../../lib/web-page"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); const EDU_THEMES = [ - // { - // title: "Création de jeux vidéo", - // description: "Conception, graphisme et animation, programmation, je vous accompagne dans la découverte des techniques pour créer un jeu vidéo de A à Z", - // image: "learning_theme_conception.png", - // pageUrl: "gamedev", - // }, { title: "Programmation", - description: `<b>Franchissez le mur du code !</b><br /> - Apprenez à programmer avec différents langages (Python, Javascript, C ...), pour du web, du logiciel, du jeu vidéo ou autre.`, + description: "edu-learn-coding", image: "learning_theme_coding.png", - // pageUrl: "coding", }, { title: "Dessin numérique et animation 2D", - description: `Apprenez à utiliser des logiciels libres de création graphique 2D et d'animation.<br /> - Créez des personnages et des décors, menez votre projet de dessin animé, d'illustration ou de jeu vidéo.`, + description: "edu-learn-2d", image: "learning_theme_2d.png", - // pageUrl: "2d", }, { title: "Maths et physique", - description: "Abordez les notions fondamentales de façon décontractée pour le plaisir de comprendre en s'appuyant sur les domaines d'application du jeu vidéo.", + description: "edu-learn-math", image: "learning_theme_math.png", - // pageUrl: "math", }, - // { - // title: "Musique et sons électroniques", - // description: "Découvrez des logiciels libres de composition musicales, de synthèse sonore et de prise de son.", - // image: "learning_theme_sound.png", - // pageUrl: "sound", - // }, { title: "Aide informatique générale", - description: "Perdu avec votre ordinateur ou votre smartphone, les logiciels, internet ? Prenez en main les fondamentaux apprenez pas à pas à utiliser sereinement la technologie.", + description: "edu-learn-computer", image: "learning_theme_pc.png", - // pageUrl: "popularization" }, { title: "Stage GNU/Linux", - description: `<b>Passez le cap du libre ! </b><br/> - Apprenez à installer Linux, faites vos premiers pas avec les logiciels libres et acquérez une autonomie suffisante pour une utilisation basique.`, + description: "edu-learn-gnu", image: "learning_theme_linux.png" }, { title: "Créer un jeu avec Mentalo", - description: "Créez un jeu en quelques séances avec l'application Mentalo. Manipulez des concepts logiques, narratifs et artistiques avec le maximum de simplicité.", + description: "edu-learn-mentalo", image: "learning_theme_mentalo.png", } ]; @@ -81,11 +64,10 @@ class EducationPage extends WebPage { }, ], }, - { tag: "h1", contents: "Pédagogie" }, + { tag: "h1", contents: t("Pédagogie") }, { tag: "p", - contents: `Ateliers, stages, workshops et cours particuliers accessibles à tous. - Programmation, graphisme 2D, jeux vidéo, vulgarisation informatique, etc.`, + contents: t("edu-page-intro"), }, ], }, @@ -112,8 +94,8 @@ class EducationPage extends WebPage { class: "edu-theme", contents: [ { tag: "img", width: 250, height: 140, class: "pixelated", src: `${images_url}/${theme.image}` }, - { tag: "h3", contents: theme.title }, - { tag: "p", contents: theme.description }, + { tag: "h3", contents: t(theme.title) }, + { tag: "p", contents: t(theme.description) }, ] } }) @@ -130,131 +112,11 @@ class EducationPage extends WebPage { tag: "div", class: "page-contents-center", contents: [ - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Pour qui ?" }, - // { - // tag: "p", - // class: "info-body", - // contents: `Les ateliers sont accessibles aux adultes comme aux enfants, plutôt à partir de 12 ans.<br/> - // Les séances ont lieu en groupes mixtes. Capacité limitée à 5 personnes. - // ` - // } - // ] - // }, - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Où ça ?" }, - // { - // tag: "p", - // class: "info-body", - // contents: "Dans mon local professionnel : <br /><blue>32 rue Simon Vialet, passage du Cheminou, 07240 Vernoux en Vivarais.</blue>" - // } - // ] - // }, - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Quel matériel ?" }, - // { - // tag: "p", - // class: "info-body", - // contents: `Le matériel informatique est fourni sur place (ordinateurs et tablettes graphique) - // mais il est possible d'amener le sien. - // <br />Il est recommandé d'apporter au moins une clé USB pour faire ses sauvegardes.` - // } - // ] - // }, - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Quand ?" }, - // { - // tag: "ul", - // class: "info-body tabled", - // contents: [ - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Mardi" }, - // { tag: "span", contents: "16h - 18h" }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Mercredi" }, - // { tag: "span", contents: "14h - 16h" }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Jeudi" }, - // { tag: "span", contents: "16h - 18h" }, - // ] - // }, - // { - // tag: "li", - // class: "fullwidth", - // contents: "<em><blue>Ouvert de Septembre à Juin, sauf vacances scolaires ou fermetures exceptionnelles</blue></em>" - // } - // ] - // }, - // ] - // }, - // { - // tag: "div", - // class: "info-block", - // contents: [ - // { tag: "h3", class: "info-title", contents: "Combien ça coûte ?" }, - // { - // tag: "ul", - // class: "info-body tabled", - // contents: [ - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Inscription au mois" }, - // { tag: "span", contents: "50€, accès à toutes les plages horaires." }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Inscription à la séance" }, - // { tag: "span", contents: "15€" }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Cours particuliers" }, - // { tag: "span", contents: "30€/h, sur place ou en visio. Horaires à définir." }, - // ] - // }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "Stage 4 séances de 2h" }, - // { tag: "span", contents: "40€ par personne, horaires et dates à définir selon la demande." }, - // ] - // } - // ] - // } - // ] - // }, { tag: "div", class: "info-block", contents: [ - { tag: "h3", class: "info-title", contents: "Pour s'inscrire ou en savoir plus <em>(programme 2021 2022 à définir, plus d'infos bientôt)</em>" }, + { tag: "h3", class: "info-title", contents: `${t("Pour s'inscrire ou en savoir plus")} <em>(programme 2022 à définir, plus d'infos bientôt)</em>` }, { tag: "ul", class: "info-body", @@ -262,7 +124,7 @@ class EducationPage extends WebPage { { tag: "li", contents: [ - { tag: "span", contents: "Me contacter" }, + { tag: "span", contents: t("Me contacter") }, { tag: "a", href: "mailto:contact@kuadrado-software.fr", @@ -282,12 +144,6 @@ class EducationPage extends WebPage { }, ] }, - // { - // tag: "li", - // contents: [ - // { tag: "span", contents: "ou passer directement me voir au local !" } - // ] - // } ] } ] diff --git a/website/src/pages/games/components/game-article.js b/website/src/pages/games/components/game-article.js index 6308e65a25509cbdc3752dfeec0b43fc4579c990..69774a810d3fba6ae32fad8626ebdf0282bffc78 100644 --- a/website/src/pages/games/components/game-article.js +++ b/website/src/pages/games/components/game-article.js @@ -149,6 +149,7 @@ class GameArticle { { tag: "label", contents: detail.label }, { tag: "div", + class: "detail-value", contents: detail.value }, ], diff --git a/website/src/pages/games/components/game-articles.js b/website/src/pages/games/components/game-articles.js index f50bc19951d94cdcdac784d1ad18a6c156c8b683..7ad6ad43703c7ab815a85cc786cba8de80e52249 100644 --- a/website/src/pages/games/components/game-articles.js +++ b/website/src/pages/games/components/game-articles.js @@ -2,6 +2,7 @@ const { loadArticles } = require("../../../lib/article-utils"); const GameArticle = require("./game-article"); +const translator = require("ks-cheap-translator"); class GameArticles { constructor(props) { @@ -9,12 +10,12 @@ class GameArticles { this.state = { articles: [], }; - this.id = performance.now(); + this.id = "game-articles-section"; this.loadArticles(); } loadArticles() { - loadArticles("games") + loadArticles("games", translator.locale) .then(articles => { this.state.articles = articles; this.refresh(); diff --git a/website/src/pages/games/games.js b/website/src/pages/games/games.js index 246ae03c5e8c9038758cea324cd11e1c5bc45436..17ffd9371763a9b788fca0ffccd70564bdf7f97a 100644 --- a/website/src/pages/games/games.js +++ b/website/src/pages/games/games.js @@ -3,6 +3,8 @@ const { images_url } = require("../../../constants"); const WebPage = require("../../lib/web-page"); const GameArticles = require("./components/game-articles"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); class GamesPage extends WebPage { render() { @@ -29,11 +31,10 @@ class GamesPage extends WebPage { }, ], }, - { tag: "h1", contents: "Jeux" }, + { tag: "h1", contents: t("Jeux") }, { tag: "p", - contents: `Création de jeux vidéos indépendants. - <br/>Jeux web, PC et projets en cours de développement`, + contents: t("games-page-intro"), }, ], }, diff --git a/website/src/pages/software-development/components/software-article.js b/website/src/pages/software-development/components/software-article.js index ae5ad4f689ec7accf942fd31aa7314428c8228b5..71da71bf542b7ddaa11a63e026cb5308aaea34b7 100644 --- a/website/src/pages/software-development/components/software-article.js +++ b/website/src/pages/software-development/components/software-article.js @@ -62,6 +62,7 @@ class SoftwareArticle { { tag: "label", contents: detail.label }, { tag: "div", + class: "detail-value", contents: detail.value }, ], diff --git a/website/src/pages/software-development/components/software-articles.js b/website/src/pages/software-development/components/software-articles.js index acc5199a8c7041e90027bc13b1af4903db82d8cd..7ec6fce092a9b13a30e5432097d695519f9e20db 100644 --- a/website/src/pages/software-development/components/software-articles.js +++ b/website/src/pages/software-development/components/software-articles.js @@ -2,6 +2,7 @@ const { loadArticles } = require("../../../lib/article-utils"); const SoftwareArticle = require("./software-article"); +const translator = require("ks-cheap-translator"); class SoftwareArticles { constructor(props) { @@ -9,12 +10,12 @@ class SoftwareArticles { this.state = { articles: [], }; - this.id = performance.now(); + this.id = "software-articles-section"; this.loadArticles(); } loadArticles() { - loadArticles("software").then(articles => { + loadArticles("software", translator.locale).then(articles => { this.state.articles = articles; this.refresh(); this.fixScroll(); diff --git a/website/src/pages/software-development/software-development.js b/website/src/pages/software-development/software-development.js index 0745aa8b08fd1e3ec62644ad72a1e0e29543e6dc..a74945d9f111226776b7d4a8af4d16344f476ac1 100644 --- a/website/src/pages/software-development/software-development.js +++ b/website/src/pages/software-development/software-development.js @@ -3,6 +3,8 @@ const { images_url } = require("../../../constants"); const WebPage = require("../../lib/web-page"); const SoftwareArticles = require("./components/software-articles"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); class SoftwareDevelopment extends WebPage { render() { @@ -32,7 +34,7 @@ class SoftwareDevelopment extends WebPage { { tag: "h1", contents: "Software" }, { tag: "p", - contents: `R&D, projets expérimentaux, outillage logiciel pour le développement de jeu ou pour le web.`, + contents: t("software-page-intro"), }, ], }, diff --git a/website/src/style.scss b/website/src/style.scss index 9fce3a9fed41050678982cc276c743b5f7a109e8..e3ca9c780a5ec05aec8bccf62914ffa7717c492b 100644 --- a/website/src/style.scss +++ b/website/src/style.scss @@ -147,6 +147,7 @@ main { margin: 0; list-style-type: none; height: 100%; + flex: 1; li { position: relative; a { @@ -199,6 +200,23 @@ main { } } } + &.lang-flags { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; + padding: 0 20px; + img { + width: 35px; + height: 30px; + cursor: pointer; + opacity: 0.5; + &.selected, + &:hover { + opacity: 1; + } + } + } } } .burger { @@ -255,6 +273,11 @@ main { } margin-left: 20px; } + &.lang-flags { + margin-left: unset; + justify-content: space-around; + padding: 20px; + } } } } @@ -391,6 +414,9 @@ main { font-weight: bold; color: $medium_grey; } + .detail-value { + text-align: right; + } } } } diff --git a/website/src/template/components/navbar.js b/website/src/template/components/navbar.js index b2ae80d3c84ec2b5c55d1f8f8533763c019ab895..74c88f187ca79d0e74af6f5cf8c2859a15ef04f7 100644 --- a/website/src/template/components/navbar.js +++ b/website/src/template/components/navbar.js @@ -1,15 +1,14 @@ "use strict"; const { images_url } = require("../../../constants"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator); const NAV_MENU_ITEMS = [ { url: "/games/", text: "Jeux" }, { url: "/education/", text: "Pédagogie", - // submenu: [ - // { url: "/gamedev", text: "Création de jeux vidéo" }, - // ] }, { url: "/software-development/", text: "Software" } ]; @@ -35,6 +34,12 @@ class NavBar { }); } + handle_chang_lang(lang) { + translator.update_translations(lang).then(() => { + obj2htm.renderCycle(); + }).catch(err => console.log(err)); + } + renderHome() { return { tag: "div", @@ -76,10 +81,20 @@ class NavBar { { tag: "a", href, - contents: text, + contents: t(text), }, ].concat(submenu ? [this.renderMenu(submenu, true, url)] : []), }; + }).concat({ + tag: "li", + class: "lang-flags", + contents: ["fr", "en"].map(lang => { + return { + tag: "img", src: `${images_url}/flag-${lang}.svg`, + class: translator.locale === lang ? "selected" : "", + onclick: this.handle_chang_lang.bind(this, lang) + } + }) }), }; } diff --git a/website/src/template/template.js b/website/src/template/template.js index fbf9f58b8be3660527beb66663c2425d834272dd..acf1dad70da772f3aa42687ee7b80218816d34cd 100644 --- a/website/src/template/template.js +++ b/website/src/template/template.js @@ -3,6 +3,8 @@ const { in_construction } = require("../../config"); const { images_url } = require("../../constants"); const NavBar = require("./components/navbar"); +const translator = require("ks-cheap-translator"); +const t = translator.trad.bind(translator) class Template { constructor(props) { @@ -23,7 +25,7 @@ class Template { { tag: "strong", class: "page-contents-center", - contents: "Site en construction ...", + contents: t("Site en construction ..."), }, ], }, @@ -73,7 +75,7 @@ class Template { contents: [ { tag: "strong", - contents: "<blue>Sur les réseaux : </blue>", + contents: `<blue>${t("Sur les réseaux")} : </blue>`, }, { tag: "a", @@ -95,11 +97,11 @@ class Template { tag: "span", contents: `Copyleft 🄯 ${new Date() .getFullYear()} Kuadrado Software | - Toutes les images du site ont été réalisées par mes soins et peuvent être réutilisées pour un usage personnel.`, + ${t("kuadrado-footer-copyleft")}`, }, { tag: "div", contents: [ - { tag: "span", contents: "Ce site web est " }, + { tag: "span", contents: t("Ce site web est") + " " }, { tag: "a", target: "_blank", style_rules: { fontWeight: "bold" },