From c8bf32542f92b60c06ca1c33e291ee7de2dbe74e Mon Sep 17 00:00:00 2001 From: Alex Lion Date: Sun, 24 Aug 2025 09:55:09 +0200 Subject: [PATCH] feat: add hu and lv locales to airpicker and moment --- CHANGELOG.md | 2 + README.md | 5 +- assets/js/app.js | 84 ++++++++++++++----- config/runtime.exs | 2 +- .../live/user_settings_live/show.html.heex | 10 ++- .../templates/layout/root.html.heex | 2 +- 6 files changed, 78 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1634f10..99e20a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ### Features - Add `LANGUAGES` setting to configure available languages in the app +- Add Latvian language support (@possible-im) +- Add Hungarian language support (@bpisch) ### Fixes and improvements diff --git a/README.md b/README.md index e56581b..aa64e25 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ Claper has a two-sided mission: - The first one is to help these people presenting an idea or a message by giving them the opportunity to make their presentation unique and to have real-time feedback from their audience. - The second one is to help each participant to take their place, to be an actor in the presentation, in the meeting and to feel important and useful. -Supported languages: ๐Ÿ‡ฌ๐Ÿ‡ง English, ๐Ÿ‡ซ๐Ÿ‡ท French, ๐Ÿ‡ฉ๐Ÿ‡ช German, ๐Ÿ‡ช๐Ÿ‡ธ Spanish, ๐Ÿ‡ณ๐Ÿ‡ฑ Dutch, ๐Ÿ‡ฎ๐Ÿ‡น Italian +Supported languages: ๐Ÿ‡ฌ๐Ÿ‡ง English, ๐Ÿ‡ซ๐Ÿ‡ท French, ๐Ÿ‡ฉ๐Ÿ‡ช German, ๐Ÿ‡ช๐Ÿ‡ธ Spanish, ๐Ÿ‡ณ๐Ÿ‡ฑ Dutch, ๐Ÿ‡ฎ๐Ÿ‡น Italian, ๐Ÿ‡ญ๐Ÿ‡บ Hungarian, ๐Ÿ‡ฑ๐Ÿ‡ป Latvian ### Built With @@ -101,6 +101,3 @@ Distributed under the GPLv3 License. See `LICENSE.txt` for more information. [Tailwind-url]: https://tailwindcss.com/ [Phoenix]: https://img.shields.io/badge/phoenix-f35424?style=for-the-badge&logo=&logoColor=white [Phoenix-url]: https://www.phoenixframework.org/ -[lmddc-logo]: /priv/static/images/partners/lmddc.png -[pixilearn-logo]: /priv/static/images/partners/pixilearn.png -[uccs-logo]: /priv/static/images/partners/uccs.png diff --git a/assets/js/app.js b/assets/js/app.js index 565496c..4f31905 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -13,11 +13,14 @@ import airdatepickerLocaleDe from "air-datepicker/locale/de"; import airdatepickerLocaleEs from "air-datepicker/locale/es"; import airdatepickerLocaleNl from "air-datepicker/locale/nl"; import airdatepickerLocaleIt from "air-datepicker/locale/it"; +import airdatepickerLocaleHu from "air-datepicker/locale/hu"; import "moment/locale/de"; import "moment/locale/fr"; import "moment/locale/es"; import "moment/locale/nl"; import "moment/locale/it"; +import "moment/locale/hu"; +import "moment/locale/lv"; import QRCodeStyling from "qr-code-styling"; import { Presenter } from "./presenter"; import { Manager } from "./manager"; @@ -26,28 +29,53 @@ import { TourGuideClient } from "@sjmc11/tourguidejs/src/Tour"; window.moment = moment; // Get supported locales from backend configuration or fallback to default list -const supportedLocales = window.claperConfig?.supportedLocales || ["en", "fr", "de", "es", "nl", "it"]; +const supportedLocales = window.claperConfig?.supportedLocales || [ + "en", + "fr", + "de", + "es", + "nl", + "it", + "hu", + "lv", +]; + +const airdatePickrSupportedLocales = window.claperConfig?.supportedLocales || [ + "en", + "fr", + "de", + "es", + "nl", + "it", + "hu", +]; var locale = document.querySelector("html").getAttribute("lang") || navigator.language.split("-")[0]; +var airdatepickrLocale = locale; + if (!supportedLocales.includes(locale)) { locale = "en"; } +if (!airdatePickrSupportedLocales.includes(locale)) { + airdatepickrLocale = "en"; +} window.moment.locale("en"); window.moment.locale(locale); window.Alpine = Alpine; Alpine.start(); -let airdatepickerLocale = { +let airdatePickrLocales = { en: airdatepickerLocaleEn, fr: airdatepickerLocaleFr, de: airdatepickerLocaleDe, es: airdatepickerLocaleEs, nl: airdatepickerLocaleNl, it: airdatepickerLocaleIt, + hu: airdatepickerLocaleHu, }; let csrfToken = document .querySelector("meta[name='csrf-token']") @@ -70,8 +98,8 @@ Hooks.EmbeddedBanner = { Hooks.TourGuide = { mounted() { this.triggerDiv = document.querySelector(this.el.dataset.btnTrigger); - this.btnTrigger = this.triggerDiv.querySelector('.open'); - this.closeBtnTrigger = this.triggerDiv.querySelector('.close'); + this.btnTrigger = this.triggerDiv.querySelector(".open"); + this.closeBtnTrigger = this.triggerDiv.querySelector(".close"); this.tour = new TourGuideClient({ nextLabel: this.el.dataset.nextLabel, @@ -106,7 +134,7 @@ Hooks.TourGuide = { destroyed() { this.btnTrigger.removeEventListener("click", () => { this.startTour(); - }); + }); this.closeBtnTrigger.removeEventListener("click", () => { this.triggerDiv.classList.add("hidden"); this.tour.finishTour(true, this.el.dataset.group); @@ -201,26 +229,36 @@ Hooks.Scroll = { Hooks.ScrollIntoDiv = { mounted() { let useParent = this.el.dataset.useParent === "true"; - this.scrollElement = this.el.dataset.useParent === "true" ? this.el.parentElement : this.el; + this.scrollElement = + this.el.dataset.useParent === "true" ? this.el.parentElement : this.el; this.checkIfAtBottom(); this.scrollToBottom(true); this.handleEvent("scroll", () => this.scrollToBottom()); this.scrollElement.addEventListener("scroll", () => this.checkIfAtBottom()); }, checkIfAtBottom() { - this.isAtBottom = this.scrollElement.scrollHeight - this.scrollElement.scrollTop - this.scrollElement.clientHeight <= 30; + this.isAtBottom = + this.scrollElement.scrollHeight - + this.scrollElement.scrollTop - + this.scrollElement.clientHeight <= + 30; }, scrollToBottom(force = false) { if (force || this.isAtBottom) { - this.scrollElement.scrollTo({ top: this.scrollElement.scrollHeight, behavior: "smooth" }); + this.scrollElement.scrollTo({ + top: this.scrollElement.scrollHeight, + behavior: "smooth", + }); } }, updated() { this.scrollToBottom(); }, destroyed() { - this.scrollElement.removeEventListener("scroll", () => this.checkIfAtBottom()); - } + this.scrollElement.removeEventListener("scroll", () => + this.checkIfAtBottom(), + ); + }, }; Hooks.NicknamePicker = { @@ -244,7 +282,7 @@ Hooks.NicknamePicker = { clicked(e) { let nickname = prompt( this.el.dataset.prompt, - localStorage.getItem("nickname") || "" + localStorage.getItem("nickname") || "", ); if (nickname) { @@ -354,7 +392,7 @@ Hooks.Pickr = { const utc = moment(date).utc().format("YYYY-MM-DDTHH:mm:ss"); utcTime.value = utc; }, - locale: airdatepickerLocale[locale], + locale: airdatePickrLocales[airdatepickrLocale], }); }, updated() {}, @@ -393,7 +431,7 @@ Hooks.OpenPresenter = { window.open( this.el.dataset.url, "newwindow", - "width=" + window.screen.width + ",height=" + window.screen.height + "width=" + window.screen.width + ",height=" + window.screen.height, ); }, mounted() { @@ -418,7 +456,12 @@ Hooks.GlobalReacts = { const container = document.createElement("div"); container.innerHTML = svgContent; const svgElement = container.firstChild; - svgElement.classList.add("react-animation", "absolute", "transform", "opacity-0"); + svgElement.classList.add( + "react-animation", + "absolute", + "transform", + "opacity-0", + ); svgElement.classList.add(...this.el.className.split(" ")); this.el.appendChild(svgElement); } @@ -430,15 +473,17 @@ Hooks.GlobalReacts = { preloadSVGs() { const svgTypes = ["heart", "hundred", "clap", "raisehand"]; - svgTypes.forEach(type => { + svgTypes.forEach((type) => { fetch(`/images/icons/${type}.svg`) - .then(response => response.text()) - .then(svgContent => { + .then((response) => response.text()) + .then((svgContent) => { this.svgCache[type] = svgContent; }) - .catch(error => console.error(`Error loading SVG for ${type}:`, error)); + .catch((error) => + console.error(`Error loading SVG for ${type}:`, error), + ); }); - } + }, }; Hooks.JoinEvent = { mounted() { @@ -630,7 +675,6 @@ window.addEventListener("phx:page-loading-stop", (info) => { topbar.hide(); }); - const onlineUserTemplate = function (user) { return `
diff --git a/config/runtime.exs b/config/runtime.exs index e5b490a..b5ec380 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -129,7 +129,7 @@ allow_unlink_external_provider = logout_redirect_url = get_var_from_path_or_env(config_dir, "LOGOUT_REDIRECT_URL", nil) languages = - get_var_from_path_or_env(config_dir, "LANGUAGES", "en,fr,es") + get_var_from_path_or_env(config_dir, "LANGUAGES", "en,fr,es,it,de") |> String.split(",") |> Enum.map(&String.trim/1) diff --git a/lib/claper_web/live/user_settings_live/show.html.heex b/lib/claper_web/live/user_settings_live/show.html.heex index e455e1c..3b21e30 100644 --- a/lib/claper_web/live/user_settings_live/show.html.heex +++ b/lib/claper_web/live/user_settings_live/show.html.heex @@ -259,11 +259,19 @@ {"English", "en"}, {"Espaรฑol", "es"}, {"Franรงais", "fr"}, + {"Hungarian", "hu"}, {"Italiano", "it"}, + {"Latvian", "lv"}, {"Nederlands", "nl"} ] |> Enum.filter(fn {_name, code} -> - code in Application.get_env(:claper, :languages, ["en", "fr", "es"]) + code in Application.get_env(:claper, :languages, [ + "en", + "fr", + "es", + "it", + "de" + ]) end) } key={:locale} diff --git a/lib/claper_web/templates/layout/root.html.heex b/lib/claper_web/templates/layout/root.html.heex index c1ead07..71fb97d 100644 --- a/lib/claper_web/templates/layout/root.html.heex +++ b/lib/claper_web/templates/layout/root.html.heex @@ -11,7 +11,7 @@