feat: add hu and lv locales to airpicker and moment

This commit is contained in:
Alex Lion
2025-08-24 09:55:09 +02:00
parent 4c678dc8df
commit c8bf32542f
6 changed files with 78 additions and 27 deletions

View File

@@ -3,6 +3,8 @@
### Features ### Features
- Add `LANGUAGES` setting to configure available languages in the app - Add `LANGUAGES` setting to configure available languages in the app
- Add Latvian language support (@possible-im)
- Add Hungarian language support (@bpisch)
### Fixes and improvements ### Fixes and improvements

View File

@@ -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 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. - 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 ### Built With
@@ -101,6 +101,3 @@ Distributed under the GPLv3 License. See `LICENSE.txt` for more information.
[Tailwind-url]: https://tailwindcss.com/ [Tailwind-url]: https://tailwindcss.com/
[Phoenix]: https://img.shields.io/badge/phoenix-f35424?style=for-the-badge&logo=&logoColor=white [Phoenix]: https://img.shields.io/badge/phoenix-f35424?style=for-the-badge&logo=&logoColor=white
[Phoenix-url]: https://www.phoenixframework.org/ [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

View File

@@ -13,11 +13,14 @@ import airdatepickerLocaleDe from "air-datepicker/locale/de";
import airdatepickerLocaleEs from "air-datepicker/locale/es"; import airdatepickerLocaleEs from "air-datepicker/locale/es";
import airdatepickerLocaleNl from "air-datepicker/locale/nl"; import airdatepickerLocaleNl from "air-datepicker/locale/nl";
import airdatepickerLocaleIt from "air-datepicker/locale/it"; import airdatepickerLocaleIt from "air-datepicker/locale/it";
import airdatepickerLocaleHu from "air-datepicker/locale/hu";
import "moment/locale/de"; import "moment/locale/de";
import "moment/locale/fr"; import "moment/locale/fr";
import "moment/locale/es"; import "moment/locale/es";
import "moment/locale/nl"; import "moment/locale/nl";
import "moment/locale/it"; import "moment/locale/it";
import "moment/locale/hu";
import "moment/locale/lv";
import QRCodeStyling from "qr-code-styling"; import QRCodeStyling from "qr-code-styling";
import { Presenter } from "./presenter"; import { Presenter } from "./presenter";
import { Manager } from "./manager"; import { Manager } from "./manager";
@@ -26,28 +29,53 @@ import { TourGuideClient } from "@sjmc11/tourguidejs/src/Tour";
window.moment = moment; window.moment = moment;
// Get supported locales from backend configuration or fallback to default list // 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 = var locale =
document.querySelector("html").getAttribute("lang") || document.querySelector("html").getAttribute("lang") ||
navigator.language.split("-")[0]; navigator.language.split("-")[0];
var airdatepickrLocale = locale;
if (!supportedLocales.includes(locale)) { if (!supportedLocales.includes(locale)) {
locale = "en"; locale = "en";
} }
if (!airdatePickrSupportedLocales.includes(locale)) {
airdatepickrLocale = "en";
}
window.moment.locale("en"); window.moment.locale("en");
window.moment.locale(locale); window.moment.locale(locale);
window.Alpine = Alpine; window.Alpine = Alpine;
Alpine.start(); Alpine.start();
let airdatepickerLocale = { let airdatePickrLocales = {
en: airdatepickerLocaleEn, en: airdatepickerLocaleEn,
fr: airdatepickerLocaleFr, fr: airdatepickerLocaleFr,
de: airdatepickerLocaleDe, de: airdatepickerLocaleDe,
es: airdatepickerLocaleEs, es: airdatepickerLocaleEs,
nl: airdatepickerLocaleNl, nl: airdatepickerLocaleNl,
it: airdatepickerLocaleIt, it: airdatepickerLocaleIt,
hu: airdatepickerLocaleHu,
}; };
let csrfToken = document let csrfToken = document
.querySelector("meta[name='csrf-token']") .querySelector("meta[name='csrf-token']")
@@ -70,8 +98,8 @@ Hooks.EmbeddedBanner = {
Hooks.TourGuide = { Hooks.TourGuide = {
mounted() { mounted() {
this.triggerDiv = document.querySelector(this.el.dataset.btnTrigger); this.triggerDiv = document.querySelector(this.el.dataset.btnTrigger);
this.btnTrigger = this.triggerDiv.querySelector('.open'); this.btnTrigger = this.triggerDiv.querySelector(".open");
this.closeBtnTrigger = this.triggerDiv.querySelector('.close'); this.closeBtnTrigger = this.triggerDiv.querySelector(".close");
this.tour = new TourGuideClient({ this.tour = new TourGuideClient({
nextLabel: this.el.dataset.nextLabel, nextLabel: this.el.dataset.nextLabel,
@@ -106,7 +134,7 @@ Hooks.TourGuide = {
destroyed() { destroyed() {
this.btnTrigger.removeEventListener("click", () => { this.btnTrigger.removeEventListener("click", () => {
this.startTour(); this.startTour();
}); });
this.closeBtnTrigger.removeEventListener("click", () => { this.closeBtnTrigger.removeEventListener("click", () => {
this.triggerDiv.classList.add("hidden"); this.triggerDiv.classList.add("hidden");
this.tour.finishTour(true, this.el.dataset.group); this.tour.finishTour(true, this.el.dataset.group);
@@ -201,26 +229,36 @@ Hooks.Scroll = {
Hooks.ScrollIntoDiv = { Hooks.ScrollIntoDiv = {
mounted() { mounted() {
let useParent = this.el.dataset.useParent === "true"; 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.checkIfAtBottom();
this.scrollToBottom(true); this.scrollToBottom(true);
this.handleEvent("scroll", () => this.scrollToBottom()); this.handleEvent("scroll", () => this.scrollToBottom());
this.scrollElement.addEventListener("scroll", () => this.checkIfAtBottom()); this.scrollElement.addEventListener("scroll", () => this.checkIfAtBottom());
}, },
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) { scrollToBottom(force = false) {
if (force || this.isAtBottom) { if (force || this.isAtBottom) {
this.scrollElement.scrollTo({ top: this.scrollElement.scrollHeight, behavior: "smooth" }); this.scrollElement.scrollTo({
top: this.scrollElement.scrollHeight,
behavior: "smooth",
});
} }
}, },
updated() { updated() {
this.scrollToBottom(); this.scrollToBottom();
}, },
destroyed() { destroyed() {
this.scrollElement.removeEventListener("scroll", () => this.checkIfAtBottom()); this.scrollElement.removeEventListener("scroll", () =>
} this.checkIfAtBottom(),
);
},
}; };
Hooks.NicknamePicker = { Hooks.NicknamePicker = {
@@ -244,7 +282,7 @@ Hooks.NicknamePicker = {
clicked(e) { clicked(e) {
let nickname = prompt( let nickname = prompt(
this.el.dataset.prompt, this.el.dataset.prompt,
localStorage.getItem("nickname") || "" localStorage.getItem("nickname") || "",
); );
if (nickname) { if (nickname) {
@@ -354,7 +392,7 @@ Hooks.Pickr = {
const utc = moment(date).utc().format("YYYY-MM-DDTHH:mm:ss"); const utc = moment(date).utc().format("YYYY-MM-DDTHH:mm:ss");
utcTime.value = utc; utcTime.value = utc;
}, },
locale: airdatepickerLocale[locale], locale: airdatePickrLocales[airdatepickrLocale],
}); });
}, },
updated() {}, updated() {},
@@ -393,7 +431,7 @@ Hooks.OpenPresenter = {
window.open( window.open(
this.el.dataset.url, this.el.dataset.url,
"newwindow", "newwindow",
"width=" + window.screen.width + ",height=" + window.screen.height "width=" + window.screen.width + ",height=" + window.screen.height,
); );
}, },
mounted() { mounted() {
@@ -418,7 +456,12 @@ Hooks.GlobalReacts = {
const container = document.createElement("div"); const container = document.createElement("div");
container.innerHTML = svgContent; container.innerHTML = svgContent;
const svgElement = container.firstChild; 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(" ")); svgElement.classList.add(...this.el.className.split(" "));
this.el.appendChild(svgElement); this.el.appendChild(svgElement);
} }
@@ -430,15 +473,17 @@ Hooks.GlobalReacts = {
preloadSVGs() { preloadSVGs() {
const svgTypes = ["heart", "hundred", "clap", "raisehand"]; const svgTypes = ["heart", "hundred", "clap", "raisehand"];
svgTypes.forEach(type => { svgTypes.forEach((type) => {
fetch(`/images/icons/${type}.svg`) fetch(`/images/icons/${type}.svg`)
.then(response => response.text()) .then((response) => response.text())
.then(svgContent => { .then((svgContent) => {
this.svgCache[type] = 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 = { Hooks.JoinEvent = {
mounted() { mounted() {
@@ -630,7 +675,6 @@ window.addEventListener("phx:page-loading-stop", (info) => {
topbar.hide(); topbar.hide();
}); });
const onlineUserTemplate = function (user) { const onlineUserTemplate = function (user) {
return ` return `
<div id="online-user"> <div id="online-user">

View File

@@ -129,7 +129,7 @@ allow_unlink_external_provider =
logout_redirect_url = get_var_from_path_or_env(config_dir, "LOGOUT_REDIRECT_URL", nil) logout_redirect_url = get_var_from_path_or_env(config_dir, "LOGOUT_REDIRECT_URL", nil)
languages = 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(",") |> String.split(",")
|> Enum.map(&String.trim/1) |> Enum.map(&String.trim/1)

View File

@@ -259,11 +259,19 @@
{"English", "en"}, {"English", "en"},
{"Español", "es"}, {"Español", "es"},
{"Français", "fr"}, {"Français", "fr"},
{"Hungarian", "hu"},
{"Italiano", "it"}, {"Italiano", "it"},
{"Latvian", "lv"},
{"Nederlands", "nl"} {"Nederlands", "nl"}
] ]
|> Enum.filter(fn {_name, code} -> |> 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) end)
} }
key={:locale} key={:locale}

View File

@@ -11,7 +11,7 @@
<link phx-track-static rel="stylesheet" href="/assets/custom.css" /> <link phx-track-static rel="stylesheet" href="/assets/custom.css" />
<script> <script>
window.claperConfig = { window.claperConfig = {
supportedLocales: <%= Jason.encode!(Application.get_env(:claper, :languages, ["en", "fr", "es"])) %> supportedLocales: <%= Jason.encode!(Application.get_env(:claper, :languages, ["en", "fr", "es", "it", "de"])) %>
}; };
</script> </script>
<script defer phx-track-static type="text/javascript" src="/assets/app.js"> <script defer phx-track-static type="text/javascript" src="/assets/app.js">