diff --git a/src/main/java/fr/titionfire/ffsaf/UserInfoProvider.java b/src/main/java/fr/titionfire/ffsaf/UserInfoProvider.java new file mode 100644 index 0000000..0360130 --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/UserInfoProvider.java @@ -0,0 +1,48 @@ +package fr.titionfire.ffsaf; + +import fr.titionfire.ffsaf.domain.service.TradService; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.container.PreMatching; +import jakarta.ws.rs.ext.Provider; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +@Provider +@PreMatching +public class UserInfoProvider implements ContainerRequestFilter { + + private static final List SUPPORTED_LANGUAGES = Arrays.asList( + Locale.FRENCH, + Locale.ENGLISH + ); + + @Override + public void filter(ContainerRequestContext requestContext) { + List acceptableLanguages = requestContext.getAcceptableLanguages(); + Locale selectedLocale = findFirstSupportedLanguage(acceptableLanguages); + + if (selectedLocale == null) + selectedLocale = TradService.fallbackLocale; + requestContext.setProperty("userLocale", selectedLocale); + } + + private Locale findFirstSupportedLanguage(List acceptableLanguages) { + for (Locale acceptableLanguage : acceptableLanguages) { + // Vérifie si la langue est dans la liste des langues supportées + if (SUPPORTED_LANGUAGES.contains(acceptableLanguage)) { + return acceptableLanguage; + } + // Vérifie aussi par tag de langue (ex: "fr-FR" -> "fr") + String languageTag = acceptableLanguage.getLanguage(); + for (Locale supportedLanguage : SUPPORTED_LANGUAGES) { + if (supportedLanguage.getLanguage().equals(languageTag)) { + return supportedLanguage; + } + } + } + return null; + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java index 5dd2283..2b5ecd1 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/AffiliationService.java @@ -79,6 +79,9 @@ public class AffiliationService { @ConfigProperty(name = "notif.affRequest.mail") List mails; + @Inject + TradService trad; + public Uni> getAllReq() { return repositoryRequest.listAll(); } @@ -92,7 +95,7 @@ public class AffiliationService { return Uni.createFrom().item(affModel) .invoke(Unchecked.consumer(model -> { if (model.getSaison() != currentSaison && model.getSaison() != currentSaison + 1) { - throw new DBadRequestException("Saison non valid"); + throw new DBadRequestException(trad.t("saison.non.valid")); } })) .chain(() -> ((affModel.getState_id().charAt(0) == 'W') ? stateIdService.get_rna( @@ -110,7 +113,7 @@ public class AffiliationService { out, affModel.getSaison())) .onItem().invoke(Unchecked.consumer(count -> { if (count != 0 && unique) { - throw new DBadRequestException("Demande d'affiliation déjà existante"); + throw new DBadRequestException(trad.t("demande.d.affiliation.deja.existante")); } })) ) @@ -118,28 +121,28 @@ public class AffiliationService { repository.count("club = ?1 and saison = ?2", club, affModel.getSaison()))) .onItem().invoke(Unchecked.consumer(count -> { if (count != 0) { - throw new DBadRequestException("Affiliation déjà existante"); + throw new DBadRequestException(trad.t("affiliation.deja.existante")); } })) .map(o -> affModel) .call(model -> ((model.getM1_lincence() != -1) ? combRepository.find("licence", model.getM1_lincence()).count().invoke(Unchecked.consumer(count -> { if (count == 0) { - throw new DBadRequestException("Licence membre n°1 inconnue"); + throw new DBadRequestException(trad.t("licence.membre.n.1.inconnue")); } })) : Uni.createFrom().nullItem()) ) .call(model -> ((model.getM2_lincence() != -1) ? combRepository.find("licence", model.getM2_lincence()).count().invoke(Unchecked.consumer(count -> { if (count == 0) { - throw new DBadRequestException("Licence membre n°2 inconnue"); + throw new DBadRequestException(trad.t("licence.membre.n.2.inconnue")); } })) : Uni.createFrom().nullItem()) ) .call(model -> ((model.getM3_lincence() != -1) ? combRepository.find("licence", model.getM3_lincence()).count().invoke(Unchecked.consumer(count -> { if (count == 0) { - throw new DBadRequestException("Licence membre n°3 inconnue"); + throw new DBadRequestException(trad.t("licence.membre.n.3.inconnue")); } })) : Uni.createFrom().nullItem()) ); @@ -148,7 +151,7 @@ public class AffiliationService { public Uni saveEdit(AffiliationRequestForm form) { return pre_save(form, false) .chain(model -> repositoryRequest.findById(form.getId()) - .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) + .onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve"))) .chain(origine -> { origine.setName(model.getName()); origine.setAddress(model.getAddress()); @@ -201,7 +204,7 @@ public class AffiliationService { LOGGER.debug(form.toString()); return repositoryRequest.findById(form.getId()) - .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) + .onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve"))) .map(model -> { model.setName(form.getName()); model.setState_id(form.getState_id()); @@ -306,7 +309,7 @@ public class AffiliationService { LOGGER.debug(form.toString()); return repositoryRequest.findById(form.getId()) - .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) + .onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve"))) .chain(req -> clubRepository.find("StateId = ?1", form.getState_id()).firstResult() .chain(model -> (model == null) ? acceptNew(form, req) : acceptOld(form, req, model)) @@ -398,7 +401,7 @@ public class AffiliationService { public Uni getRequest(long id) { return repositoryRequest.findById(id).map(SimpleReqAffiliation::fromModel) - .onItem().ifNull().failWith(new DNotFoundException("Demande d'affiliation non trouvé")) + .onItem().ifNull().failWith(new DNotFoundException(trad.t("demande.d.affiliation.non.trouve"))) .call(out -> clubRepository.find("StateId = ?1", out.getStateId()).firstResult().invoke(c -> { if (c != null) { out.setClub(c.getId()); @@ -422,7 +425,7 @@ public class AffiliationService { public Uni> getAffiliation(long id) { return clubRepository.findById(id) - .onItem().ifNull().failWith(new DNotFoundException("Club non trouvé")) + .onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve"))) .call(model -> Mutiny.fetch(model.getAffiliations())) .chain(model -> repositoryRequest.list("state_id = ?1", model.getStateId()) .map(reqs -> reqs.stream().map(req -> @@ -434,11 +437,11 @@ public class AffiliationService { public Uni setAffiliation(long id, int saison) { return clubRepository.findById(id) - .onItem().ifNull().failWith(new DNotFoundException("Club non trouvé")) + .onItem().ifNull().failWith(new DNotFoundException(trad.t("club.non.trouve"))) .call(model -> Mutiny.fetch(model.getAffiliations())) .invoke(Unchecked.consumer(club -> { if (club.getAffiliations().stream().anyMatch(affiliation -> affiliation.getSaison() == saison)) { - throw new DBadRequestException("Affiliation déjà existante"); + throw new DBadRequestException(trad.t("affiliation.deja.existante")); } })) .chain(club -> diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CategoryService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CategoryService.java index 0485ede..4b54ef1 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CategoryService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CategoryService.java @@ -45,10 +45,13 @@ public class CategoryService { @Inject CompetitionGuestRepository competitionGuestRepository; + @Inject + TradService trad; + public Uni getByIdAdmin(SecurityCtx securityCtx, CompetitionSystem system, Long id) { return repository.find("systemId = ?1 AND system = ?2", id, system) .firstResult() - .onItem().ifNull().failWith(() -> new RuntimeException("Category not found")) + .onItem().ifNull().failWith(() -> new RuntimeException(trad.t("categorie.non.trouver"))) .call(data -> permService.hasAdminViewPerm(securityCtx, data.getCompet())) .map(CategoryData::fromModel); } @@ -64,7 +67,7 @@ public class CategoryService { .chain(o -> { if (o == null) { return competRepository.findById(data.getCompet()) - .onItem().ifNull().failWith(() -> new RuntimeException("Competition not found")) + .onItem().ifNull().failWith(() -> new RuntimeException(trad.t("competition.not.found"))) .call(o2 -> permService.hasEditPerm(securityCtx, o2)) .chain(competitionModel -> { CategoryModel model = new CategoryModel(); @@ -139,7 +142,7 @@ public class CategoryService { .onItem().ifNotNull().call(o2 -> permService.hasEditPerm(securityCtx, o2.getCompet())) .onItem().ifNull().switchTo( () -> competRepository.findById(data.getCompet()) - .onItem().ifNull().failWith(() -> new RuntimeException("Compet not found")) + .onItem().ifNull().failWith(() -> new RuntimeException(trad.t("competition.not.found"))) .call(o -> permService.hasEditPerm(securityCtx, o)) .map(o -> { CategoryModel model = new CategoryModel(); @@ -256,7 +259,7 @@ public class CategoryService { public Uni delete(SecurityCtx securityCtx, CompetitionSystem system, Long id) { return repository.find("systemId = ?1 AND system = ?2", id, system).firstResult() - .onItem().ifNull().failWith(() -> new RuntimeException("Category not found")) + .onItem().ifNull().failWith(() -> new RuntimeException(trad.t("categorie.non.trouver"))) .call(o -> permService.hasEditPerm(securityCtx, o.getCompet())) .call(o -> Mutiny.fetch(o.getTree()) .call(o2 -> o2.isEmpty() ? Uni.createFrom().nullItem() : diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java index f790dbe..ddba917 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ClubService.java @@ -63,6 +63,9 @@ public class ClubService { @Inject LoggerService ls; + @Inject + TradService trad; + public SimpleClubModel findByIdOptionalClub(long id) throws Throwable { return VertxContextSupport.subscribeAndAwait( () -> Panache.withTransaction(() -> repository.findById(id).map(SimpleClubModel::fromModel))); @@ -130,7 +133,7 @@ public class ClubService { return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult() .invoke(Unchecked.consumer(m -> { if (m == null || m.getClub() == null) - throw new DNotFoundException("Club non trouvé"); + throw new DNotFoundException(trad.t("club.non.trouve")); })) .map(MembreModel::getClub) .call(club -> Mutiny.fetch(club.getContact())); @@ -150,7 +153,7 @@ public class ClubService { return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult() .invoke(Unchecked.consumer(m -> { if (m == null || m.getClub() == null) - throw new DNotFoundException("Club non trouvé"); + throw new DNotFoundException(trad.t("club.non.trouve")); if (!securityCtx.isInClubGroup(m.getClub().getId())) throw new DForbiddenException(); })) @@ -166,7 +169,7 @@ public class ClubService { return combRepository.find("userId = ?1", securityCtx.getSubject()).firstResult() .invoke(Unchecked.consumer(m -> { if (m == null || m.getClub() == null) - throw new DNotFoundException("Club non trouvé"); + throw new DNotFoundException(trad.t("club.non.trouve")); if (!securityCtx.isInClubGroup(m.getClub().getId())) throw new DForbiddenException(); })) @@ -183,7 +186,7 @@ public class ClubService { ls.logUpdate("Contact(s)...", club); club.setContact(MAPPER.readValue(form.getContact(), typeRef)); } catch (JsonProcessingException e) { - throw new DBadRequestException("Erreur de format des contacts"); + throw new DBadRequestException(trad.t("erreur.de.format.des.contacts")); } ls.logChange("Lieux d'entrainements", club.getTraining_location(), form.getTraining_location(), @@ -233,7 +236,7 @@ public class ClubService { ls.logUpdate("Contact(s)...", m); m.setContact(MAPPER.readValue(input.getContact(), typeRef)); } catch (JsonProcessingException e) { - throw new DBadRequestException("Erreur de format des contacts"); + throw new DBadRequestException(trad.t("erreur.de.format.des.contacts")); } } return Panache.withTransaction(() -> repository.persist(m)).call(() -> ls.append()); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java index 332d93f..a56026f 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/CompetitionService.java @@ -97,6 +97,9 @@ public class CompetitionService { @CacheName("have-access") Cache cacheNoneAccess; + @Inject + TradService trad; + public Uni getById(SecurityCtx securityCtx, Long id) { return permService.hasViewPerm(securityCtx, id).map(cm -> { CompetitionData out = CompetitionData.fromModelLight(cm); @@ -192,7 +195,7 @@ public class CompetitionService { .invoke(Unchecked.consumer(combModel -> { if (!securityCtx.getRoles().contains("create_compet") && !securityCtx.getRoles() .contains("federation_admin")) - throw new DForbiddenException("Vous ne pouvez pas créer de compétition"); + throw new DForbiddenException(trad.t("vous.ne.pouvez.pas.creer.de.competition")); })) .map(MembreModel::getClub) .chain(clubModel -> { @@ -223,7 +226,8 @@ public class CompetitionService { keycloakService.getUser(data.getOwner()).map(UserRepresentation::getId).orElse(null)) .invoke(Unchecked.consumer(newOwner -> { if (newOwner == null) - throw new DBadRequestException("User " + data.getOwner() + " not found"); + throw new DBadRequestException( + String.format(trad.t("user.not.found"), data.getOwner())); if (!newOwner.equals(model.getOwner())) { if (!securityCtx.roleHas("federation_admin") && !securityCtx.roleHas("safca_super_admin") @@ -342,7 +346,7 @@ public class CompetitionService { || !securityCtx.isClubAdmin()) throw new DForbiddenException(); if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister())) - throw new DBadRequestException("Inscription fermée"); + throw new DBadRequestException(trad.t("inscription.fermee")); })) .chain(c -> findComb(data.getLicence(), data.getFname(), data.getLname()) .call(combModel -> Mutiny.fetch(combModel.getLicences())) @@ -350,8 +354,7 @@ public class CompetitionService { if (!securityCtx.isInClubGroup(model.getClub().getId())) throw new DForbiddenException(); if (c.getBanMembre().contains(model.getId())) - throw new DForbiddenException( - "Vous n'avez pas le droit d'inscrire ce membre (par décision de l'administrateur de la compétition)"); + throw new DForbiddenException(trad.t("insc.err1")); })) .chain(combModel -> updateRegister(data, c, combModel, false))) .map(r -> SimpleRegisterComb.fromModel(r, r.getMembre().getLicences())); @@ -361,13 +364,12 @@ public class CompetitionService { if (cm.getRegisterMode() != RegisterMode.FREE) throw new DForbiddenException(); if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister())) - throw new DBadRequestException("Inscription fermée"); + throw new DBadRequestException(trad.t("inscription.fermee")); })) .chain(c -> membreService.getByAccountId(securityCtx.getSubject()) .invoke(Unchecked.consumer(model -> { if (c.getBanMembre().contains(model.getId())) - throw new DForbiddenException( - "Vous n'avez pas le droit de vous inscrire (par décision de l'administrateur de la compétition)"); + throw new DForbiddenException(trad.t("insc.err2")); })) .chain(combModel -> updateRegister(data, c, combModel, false))) .map(r -> SimpleRegisterComb.fromModel(r, List.of())); @@ -380,8 +382,7 @@ public class CompetitionService { .map(Unchecked.function(r -> { if (r != null) { if (!admin && r.isLockEdit()) - throw new DForbiddenException( - "Modification bloquée par l'administrateur de la compétition"); + throw new DForbiddenException(trad.t("insc.err3")); r.setWeight(data.getWeight()); r.setOverCategory(data.getOverCategory()); r.setCategorie( @@ -424,17 +425,17 @@ public class CompetitionService { return combRepository.find("licence = ?1", licence).firstResult() .invoke(Unchecked.consumer(combModel -> { if (combModel == null) - throw new DForbiddenException("Licence " + licence + " non trouvé"); + throw new DForbiddenException(String.format(trad.t("licence.non.trouve"), licence)); })); } else { if (fname == null || lname == null) - return Uni.createFrom().failure(new DBadRequestException("Nom et prénom requis")); + return Uni.createFrom().failure(new DBadRequestException(trad.t("nom.et.prenom.requis"))); return combRepository.find("unaccent(lname) ILIKE unaccent(?1) AND unaccent(fname) ILIKE unaccent(?2)", lname, fname).firstResult() .invoke(Unchecked.consumer(combModel -> { if (combModel == null) - throw new DForbiddenException("Combattant " + fname + " " + lname + " non trouvé"); + throw new DForbiddenException(String.format(trad.t("combattant.non.trouve"), fname, lname)); })); } } @@ -461,12 +462,12 @@ public class CompetitionService { || !securityCtx.isClubAdmin()) throw new DForbiddenException(); if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister())) - throw new DBadRequestException("Inscription fermée"); + throw new DBadRequestException(trad.t("inscription.fermee")); })) .call(cm -> membreService.getById(combId) .invoke(Unchecked.consumer(model -> { if (model == null) - throw new DNotFoundException("Membre " + combId + " n'existe pas"); + throw new DNotFoundException(String.format(trad.t("le.membre.n.existe.pas"), combId)); if (!securityCtx.isInClubGroup(model.getClub().getId())) throw new DForbiddenException(); }))) @@ -478,7 +479,7 @@ public class CompetitionService { if (cm.getRegisterMode() != RegisterMode.FREE || !Objects.equals(model.getId(), combId)) throw new DForbiddenException(); if (new Date().before(cm.getStartRegister()) || new Date().after(cm.getEndRegister())) - throw new DBadRequestException("Inscription fermée"); + throw new DBadRequestException(trad.t("inscription.fermee")); }))) .chain(c -> deleteRegister(combId, c, false)); } @@ -486,7 +487,7 @@ public class CompetitionService { private Uni deleteRegister(Long combId, CompetitionModel c, boolean admin) { if (admin && combId < 0) { return competitionGuestRepository.find("competition = ?1 AND id = ?2", c, combId * -1).firstResult() - .onFailure().transform(t -> new DBadRequestException("Combattant non inscrit")) + .onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit"))) .call(Unchecked.function( model -> Panache.withTransaction(() -> competitionGuestRepository.delete(model)) .call(r -> c.getSystem() == CompetitionSystem.INTERNAL ? @@ -495,10 +496,10 @@ public class CompetitionService { .replaceWithVoid(); } return registerRepository.find("competition = ?1 AND membre.id = ?2", c, combId).firstResult() - .onFailure().transform(t -> new DBadRequestException("Combattant non inscrit")) + .onFailure().transform(t -> new DBadRequestException(trad.t("combattant.non.inscrit"))) .call(Unchecked.function(registerModel -> { if (!admin && registerModel.isLockEdit()) - throw new DForbiddenException("Modification bloquée par l'administrateur de la compétition"); + throw new DForbiddenException(trad.t("insc.err3")); return Panache.withTransaction(() -> registerRepository.delete(registerModel)) .call(r -> c.getSystem() == CompetitionSystem.INTERNAL ? sRegister.sendRegisterRemove(c.getUuid(), combId) : Uni.createFrom().voidItem()); @@ -533,7 +534,7 @@ public class CompetitionService { return permService.hasEditPerm(securityCtx, id) .invoke(Unchecked.consumer(cm -> { if (cm.getSystem() != CompetitionSystem.INTERNAL) - throw new DBadRequestException("Competition is not INTERNAL"); + throw new DBadRequestException(trad.t("competition.is.not.internal")); })) .chain(competitionModel -> { SimpleCompetData data = SimpleCompetData.fromModel(competitionModel); @@ -559,7 +560,7 @@ public class CompetitionService { return permService.hasEditPerm(securityCtx, data.getId()) .invoke(Unchecked.consumer(cm -> { if (cm.getSystem() != CompetitionSystem.INTERNAL) - throw new DBadRequestException("Competition is not INTERNAL"); + throw new DBadRequestException(trad.t("competition.is.not.internal")); })) .chain(cm -> vertx.getOrCreateContext().executeBlocking(() -> { ArrayList admin = new ArrayList<>(); @@ -567,13 +568,13 @@ public class CompetitionService { for (String username : data.getAdmin()) { Optional opt = keycloakService.getUser(username); if (opt.isEmpty()) - throw new DBadRequestException("User " + username + " not found"); + throw new DBadRequestException(String.format(trad.t("user.not.found"), username)); admin.add(opt.get().getId()); } for (String username : data.getTable()) { Optional opt = keycloakService.getUser(username); if (opt.isEmpty()) - throw new DBadRequestException("User " + username + " not found"); + throw new DBadRequestException(String.format(trad.t("user.not.found"), username)); table.add(opt.get().getId()); } @@ -623,13 +624,13 @@ public class CompetitionService { for (String username : data.getAdmin()) { Optional opt = keycloakService.getUser(username); if (opt.isEmpty()) - throw new DBadRequestException("User " + username + " not found"); + throw new DBadRequestException(String.format(trad.t("user.not.found"), username)); admin.add(UUID.fromString(opt.get().getId())); } for (String username : data.getTable()) { Optional opt = keycloakService.getUser(username); if (opt.isEmpty()) - throw new DBadRequestException("User " + username + " not found"); + throw new DBadRequestException(String.format(trad.t("user.not.found"), username)); table.add(UUID.fromString(opt.get().getId())); } diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java index 83e9f90..79335d9 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/LicenceService.java @@ -47,6 +47,9 @@ public class LicenceService { @Inject CheckoutService checkoutService; + @Inject + TradService trad; + public Uni> getLicence(long id, Consumer checkPerm) { return combRepository.findById(id).invoke(checkPerm) .chain(combRepository -> Mutiny.fetch(combRepository.getLicences())); @@ -157,7 +160,7 @@ public class LicenceService { return repository.list("membre.id IN ?1 AND saison = ?2 AND pay = FALSE", ids, Utils.getSaison()) .invoke(Unchecked.consumer(models -> { if (models.size() != ids.size()) - throw new DBadRequestException("Erreur lors de la sélection des membres"); + throw new DBadRequestException(trad.t("erreur.lors.de.la.selection.des.membres")); })) .call(models -> { Uni uni = Uni.createFrom().nullItem(); @@ -174,7 +177,7 @@ public class LicenceService { .call(__ -> checkoutService.canDeleteLicence(id) .invoke(Unchecked.consumer(b -> { if (!b) throw new DBadRequestException( - "Impossible de supprimer une licence pour laquelle un paiement est en cours"); + trad.t("licence.rm.err1")); }))) .call(model -> ls.logADelete(model)) .chain(model -> Panache.withTransaction(() -> repository.delete(model))); @@ -186,7 +189,7 @@ public class LicenceService { return repository.find("saison = ?1 AND membre = ?2", Utils.getSaison(), membreModel).count() .invoke(Unchecked.consumer(count -> { if (count > 0) - throw new DBadRequestException("Licence déjà demandée"); + throw new DBadRequestException(trad.t("licence.deja.demandee")); })).chain(__ -> combRepository.findById(id).chain(membreModel2 -> { LicenceModel model = new LicenceModel(); model.setClub_id((membreModel2.getClub() == null) ? null : membreModel2.getClub().getId()); @@ -215,13 +218,13 @@ public class LicenceService { .call(__ -> checkoutService.canDeleteLicence(id) .invoke(Unchecked.consumer(b -> { if (!b) throw new DBadRequestException( - "Impossible de supprimer une licence pour laquelle un paiement est en cours"); + trad.t("licence.rm.err1")); }))) .invoke(Unchecked.consumer(licenceModel -> { if (licenceModel.isValidate()) - throw new DBadRequestException("Impossible de supprimer une licence déjà validée"); + throw new DBadRequestException(trad.t("impossible.de.supprimer.une.licence.deja.validee")); if (licenceModel.isPay()) - throw new DBadRequestException("Impossible de supprimer une licence déjà payée"); + throw new DBadRequestException(trad.t("impossible.de.supprimer.une.licence.deja.payee")); })) .call(model -> ls.logADelete(model)) .chain(__ -> Panache.withTransaction(() -> repository.deleteById(id))); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java index 39fc2b2..fa40500 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/MembreService.java @@ -62,6 +62,9 @@ public class MembreService { @Inject LoggerService ls; + @Inject + TradService trad; + public SimpleCombModel find(int licence, String np) throws Throwable { return VertxContextSupport.subscribeAndAwait(() -> Panache.withTransaction(() -> repository.find( @@ -177,7 +180,7 @@ public class MembreService { Sort sort = getSort(order); if (sort == null) - return Uni.createFrom().failure(new DInternalError("Erreur lors calcul du trie")); + return Uni.createFrom().failure(new DInternalError(trad.t("erreur.lors.calcul.du.trie"))); String finalSearch = search; return getLicenceListe(licenceRequest, payState) @@ -196,7 +199,7 @@ public class MembreService { .call(result -> query.count().invoke(result::setResult_count)) .call(result -> query.pageCount() .invoke(Unchecked.consumer(pages -> { - if (page > pages) throw new DBadRequestException("Page out of range"); + if (page > pages) throw new DBadRequestException(trad.t("page.out.of.range")); })) .invoke(result::setPage_count)) .call(result -> query.page(Page.of(page, limit)).list() @@ -241,8 +244,8 @@ public class MembreService { for (MembreModel membreModel : membres) { if (!Objects.equals(membreModel.getClub(), clubModel.get())) { LOGGER.info("Similar membres found: " + membreModel); - throw new DForbiddenException( - "Le membre n°" + membreModel.getLicence() + " n'appartient pas à votre club"); + throw new DForbiddenException(String.format(trad.t("le.membre.appartient.pas.a.votre.club"), + membreModel.getLicence())); } } Uni uniResult = Uni.createFrom().voidItem(); @@ -269,7 +272,8 @@ public class MembreService { if (model.getLicence() != null && !model.getLicence().equals(dataIn.getLicence())) { LOGGER.info("Similar membres found: " + model); throw new DBadRequestException( - "Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname()); + String.format(trad.t("email.deja.utilise.par"), model.getEmail(), + model.getFname(), model.getLname())); } if (StringSimilarity.similarity(model.getLname().toUpperCase(), @@ -277,7 +281,8 @@ public class MembreService { model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3) { LOGGER.info("Similar membres found: " + model); throw new DBadRequestException( - "Email '" + model.getEmail() + "' déja utilisé par " + model.getFname() + " " + model.getFname()); + String.format(trad.t("email.deja.utilise.par"), model.getEmail(), + model.getFname(), model.getLname())); } } @@ -288,8 +293,8 @@ public class MembreService { model.getFname().toUpperCase(), dataIn.getPrenom().toUpperCase()) > 3)) { LOGGER.info("Similar membres found: " + model); throw new DBadRequestException( - "Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (tentative de changement non-autotiser de nom sur la licence " - + model.getLicence() + " pour " + model.getFname() + " " + model.getFname() + ")"); + String.format(trad.t("try.edit.licence"), model.getLicence(), model.getFname(), + model.getFname())); } ls.logChange("Nom", model.getLname(), dataIn.getNom().toUpperCase(), model); @@ -363,7 +368,7 @@ public class MembreService { .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .invoke(Unchecked.consumer(c -> { if (c > 0 && !membre.getEmail().isBlank()) - throw new DBadRequestException("Email déjà utilisé"); + throw new DBadRequestException(trad.t("email.deja.utilise")); }))) .chain(membreModel -> clubRepository.findById(membre.getClub()) .map(club -> new Pair<>(membreModel, club))) @@ -385,7 +390,7 @@ public class MembreService { .call(__ -> repository.count("email LIKE ?1 AND id != ?2", membre.getEmail(), id) .invoke(Unchecked.consumer(c -> { if (c > 0 && !membre.getEmail().isBlank()) - throw new DBadRequestException("Email déjà utilisé"); + throw new DBadRequestException(trad.t("email.deja.utilise")); }))) .invoke(Unchecked.consumer(membreModel -> { if (!securityCtx.isInClubGroup(membreModel.getClub().getId())) @@ -394,7 +399,7 @@ public class MembreService { membre.getLname().toUpperCase()) > 3 || StringSimilarity.similarity( membreModel.getFname().toUpperCase(), membre.getFname().toUpperCase()) > 3) { throw new DBadRequestException( - "Pour enregistrer un nouveau membre, veuillez utilisez le bouton prévue a cette effet."); + trad.t("regiter.new.membre")); } })) .invoke(Unchecked.consumer(membreModel -> { @@ -403,7 +408,7 @@ public class MembreService { else if (securityCtx.roleHas("club_secretaire")) source = RoleAsso.SECRETAIRE; else if (securityCtx.roleHas("club_respo_intra")) source = RoleAsso.MEMBREBUREAU; if (!membre.getRole().equals(membreModel.getRole()) && membre.getRole().level >= source.level) - throw new DForbiddenException("Permission insuffisante"); + throw new DForbiddenException(trad.t("permission.insuffisante")); })) .onItem().transform(target -> { if (!securityCtx.getSubject().equals(target.getUserId())) { @@ -477,7 +482,7 @@ public class MembreService { .call(__ -> repository.count("email LIKE ?1", input.getEmail()) .invoke(Unchecked.consumer(c -> { if (c > 0 && input.getEmail() != null && !input.getEmail().isBlank()) - throw new DBadRequestException("Email déjà utilisé"); + throw new DBadRequestException(trad.t("email.deja.utilise")); }))) .chain(clubModel -> { MembreModel model = getMembreModel(input, clubModel); @@ -493,7 +498,7 @@ public class MembreService { return repository.find("userId = ?1", subject).firstResult() .call(__ -> repository.count("email LIKE ?1", input.getEmail()) .invoke(Unchecked.consumer(c -> { - if (c > 0) throw new DBadRequestException("Email déjà utilisé"); + if (c > 0) throw new DBadRequestException(trad.t("email.deja.utilise")); }))) .call(membreModel -> repository.count( @@ -501,7 +506,7 @@ public class MembreService { input.getLname(), input.getFname(), membreModel.getClub()) .invoke(Unchecked.consumer(c -> { if (c > 0) - throw new DBadRequestException("Membre déjà existent"); + throw new DBadRequestException(trad.t("membre.deja.existent")); }))) .chain(membreModel -> { MembreModel model = getMembreModel(input, membreModel.getClub()); @@ -534,13 +539,13 @@ public class MembreService { .invoke(Unchecked.consumer(membreModel -> { if (membreModel.getLicence() != null) { throw new DBadRequestException( - "Impossible de supprimer un membre qui a déjà un numéro de licence"); + trad.t("membre.rm.err1")); } })) .call(membreModel -> licenceRepository.find("membre = ?1", membreModel).count() .invoke(Unchecked.consumer(l -> { if (l > 0) - throw new DBadRequestException("Impossible de supprimer un membre avec des licences"); + throw new DBadRequestException(trad.t("membre.rm.err2")); }))) .call(membreModel -> (membreModel.getUserId() != null) ? keycloakService.removeAccount(membreModel.getUserId()) : Uni.createFrom().nullItem()) @@ -586,7 +591,7 @@ public class MembreService { public Uni getMembre(String subject) { MeData meData = new MeData(); return repository.find("userId = ?1", subject).firstResult() - .invoke(meData::setMembre) + .invoke(m -> meData.setMembre(m, trad)) .call(membreModel -> Mutiny.fetch(membreModel.getLicences()) .map(licences -> licences.stream().map(SimpleLicence::fromModel).toList()) .invoke(meData::setLicences)) diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/PDFService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/PDFService.java index e95bb8a..fa6068b 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/PDFService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/PDFService.java @@ -46,6 +46,9 @@ public class PDFService { @ConfigProperty(name = "pdf-maker.sign-file") String sign_file; + @Inject + TradService trad; + public Uni getLicencePdf(String subject) { return getLicencePdf(combRepository.find("userId = ?1", subject).firstResult() @@ -58,7 +61,7 @@ public class PDFService { LicenceModel licence = m.getLicences().stream() .filter(licenceModel -> licenceModel.getSaison() == Utils.getSaison() && licenceModel.isValidate()) .findFirst() - .orElseThrow(() -> new DNotFoundException("Pas de licence pour la saison en cours")); + .orElseThrow(() -> new DNotFoundException(trad.t("pas.de.licence.pour.la.saison.en.cours"))); try { byte[] buff = make_pdf(m, licence); @@ -119,7 +122,7 @@ public class PDFService { combRepository.find("userId = ?1", subject).firstResult() .invoke(Unchecked.consumer(m -> { if (m == null || m.getClub() == null) - throw new DNotFoundException("Club non trouvé"); + throw new DNotFoundException(trad.t("club.non.trouve")); })) .map(MembreModel::getClub) .call(m -> Mutiny.fetch(m.getAffiliations()))); @@ -130,7 +133,7 @@ public class PDFService { clubRepository.findById(id) .invoke(Unchecked.consumer(m -> { if (m == null) - throw new DNotFoundException("Club non trouvé"); + throw new DNotFoundException(trad.t("club.non.trouve")); })) .call(m -> Mutiny.fetch(m.getAffiliations()))); } @@ -141,7 +144,7 @@ public class PDFService { .map(Unchecked.function(m -> { if (m.getAffiliations().stream() .noneMatch(licenceModel -> licenceModel.getSaison() == Utils.getSaison())) - throw new DNotFoundException("Pas d'affiliation pour la saison en cours"); + throw new DNotFoundException(trad.t("pas.d.affiliation.pour.la.saison.en.cours")); try { byte[] buff = make_pdf(m); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java index ca07ad5..f370f38 100644 --- a/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/ResultService.java @@ -45,7 +45,8 @@ public class ResultService { @Inject MatchRepository matchRepository; - private static final ResourceBundle BUNDLE = ResourceBundle.getBundle("lang.String"); + @Inject + TradService trad; private static final HashMap combTempIds = new HashMap<>(); @@ -289,17 +290,17 @@ public class ResultService { if (register.isPresent()) { categorie = register.get().getCategorie2(); ClubModel club = register.get().getClub2(); - clubName = (club == null) ? BUNDLE.getString("no.licence") : club.getName(); + clubName = (club == null) ? trad.t("no.licence") : club.getName(); } else if (comb instanceof CompetitionGuestModel guestModel) { categorie = guestModel.getCategorie(); clubName = guestModel.getClub(); } else if (comb instanceof MembreModel model) { categorie = model.getCategorie(); - clubName = (model.getClub() == null) ? BUNDLE.getString( - "no.licence") : model.getClub().getName(); + clubName = (model.getClub() == null) ? trad.t("no.licence") + : model.getClub().getName(); } - builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.cat((categorie == null) ? "---" : categorie.getName(trad)); builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY)); builder2.w(w.get()); builder2.l(l.get()); @@ -364,7 +365,7 @@ public class ResultService { Long id = getCombTempId(combTempId); if (id == null) { - return Uni.createFrom().failure(new DForbiddenException("Comb not found")); + return Uni.createFrom().failure(new DForbiddenException(trad.t("comb.not.found"))); } Uni> uni; @@ -373,13 +374,13 @@ public class ResultService { uuid, privacy).firstResult() .chain(Unchecked.function(registerModel -> { if (registerModel == null) - throw new DBadRequestException("Combattant non inscrit"); + throw new DBadRequestException(trad.t("combattant.non.inscrit")); builder.name(Utils.getFullName(registerModel.getMembre())); - builder.club((registerModel.getClub2() == null) ? BUNDLE.getString( - "no.licence") : registerModel.getClub2().getName()); - builder.cat((registerModel.getCategorie2() == null) ? "---" : registerModel.getCategorie2() - .getName(BUNDLE)); + builder.club((registerModel.getClub2() == null) ? trad.t("no.licence") : + registerModel.getClub2().getName()); + builder.cat((registerModel.getCategorie2() == null) ? "---" : + registerModel.getCategorie2().getName(trad)); return matchRepository.list("category.compet.uuid = ?1 AND (c1_id = ?2 OR c2_id = ?2)", uuid, registerModel.getMembre()); @@ -389,8 +390,8 @@ public class ResultService { .chain(guestModel -> { builder.name(Utils.getFullName(guestModel)); builder.club(guestModel.getClub()); - builder.cat((guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie() - .getName(BUNDLE)); + builder.cat( + (guestModel.getCategorie() == null) ? "---" : guestModel.getCategorie().getName(trad)); return matchRepository.list("category.compet.uuid = ?1 AND (c1_guest = ?2 OR c2_guest = ?2)", uuid, guestModel); @@ -529,13 +530,13 @@ public class ResultService { if (id < 0) { String clubName = getClubTempId(id); if (clubName == null) { - return Uni.createFrom().failure(new DForbiddenException("Club not found")); + return Uni.createFrom().failure(new DForbiddenException(trad.t("club.not.found"))); } return competitionGuestRepository.list("competition.uuid = ?1 AND club = ?2", uuid, clubName) .call(list -> { if (list.isEmpty()) - return Uni.createFrom().failure(new DBadRequestException("Club not found")); + return Uni.createFrom().failure(new DBadRequestException(trad.t("club.not.found"))); return Uni.createFrom().voidItem(); }) .chain(guests -> matchRepository.list( @@ -584,7 +585,7 @@ public class ResultService { categorie = model.getCategorie(); } - builder2.cat((categorie == null) ? "---" : categorie.getName(BUNDLE)); + builder2.cat((categorie == null) ? "---" : categorie.getName(trad)); builder2.name(comb.getName(membreModel, ResultPrivacy.REGISTERED_ONLY)); builder2.w(w.get()); builder2.l(l.get()); @@ -668,7 +669,8 @@ public class ResultService { uuid, securityCtx.getSubject()) .chain(c2 -> { if (c2 > 0) return Uni.createFrom().item(m); - return Uni.createFrom().failure(new DForbiddenException("Access denied")); + return Uni.createFrom().failure(new DForbiddenException( + trad.t("access.denied"))); }); }); } else { @@ -676,7 +678,7 @@ public class ResultService { securityCtx.getSubject()) .chain(c2 -> { if (c2 > 0) return Uni.createFrom().item(m); - return Uni.createFrom().failure(new DForbiddenException("Access denied")); + return Uni.createFrom().failure(new DForbiddenException(trad.t("access.denied"))); }); } }); diff --git a/src/main/java/fr/titionfire/ffsaf/domain/service/TradService.java b/src/main/java/fr/titionfire/ffsaf/domain/service/TradService.java new file mode 100644 index 0000000..c1102ab --- /dev/null +++ b/src/main/java/fr/titionfire/ffsaf/domain/service/TradService.java @@ -0,0 +1,41 @@ +package fr.titionfire.ffsaf.domain.service; + +import jakarta.enterprise.context.RequestScoped; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.ws.rs.container.ContainerRequestContext; + +import java.util.Locale; +import java.util.MissingResourceException; +import java.util.ResourceBundle; + +@RequestScoped +public class TradService { + + @Inject + Instance requestContextInstance; + + public static final Locale fallbackLocale = Locale.FRANCE; + + public String t(String key) { + return translate(key); + } + + public String translate(String key) { + ContainerRequestContext requestContext = requestContextInstance.get(); + Locale userLocale = (Locale) requestContext.getProperty("userLocale"); + + + try { + ResourceBundle messages = ResourceBundle.getBundle("lang.messages", userLocale); + return messages.getString(key); + } catch (MissingResourceException e) { + try { + ResourceBundle fallbackMessages = ResourceBundle.getBundle("lang.messages", fallbackLocale); + return fallbackMessages.getString(key); + } catch (MissingResourceException ex) { + return key; + } + } + } +} diff --git a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java index 42b8956..59af8db 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/AssoEndpoints.java @@ -1,10 +1,12 @@ package fr.titionfire.ffsaf.rest; +import fr.titionfire.ffsaf.domain.service.TradService; import fr.titionfire.ffsaf.rest.client.SirenService; import fr.titionfire.ffsaf.rest.client.StateIdService; import fr.titionfire.ffsaf.rest.data.AssoData; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import io.smallrye.mutiny.Uni; +import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.openapi.annotations.Operation; @@ -19,6 +21,9 @@ public class AssoEndpoints { @RestClient SirenService sirenService; + @Inject + TradService trad; + @GET @Path("state_id/{stateId}") @Produces(MediaType.APPLICATION_JSON) @@ -28,9 +33,9 @@ public class AssoEndpoints { stateId).chain(stateIdService::getAssoDataFromUnit)).onFailure().transform(throwable -> { if (throwable instanceof WebApplicationException exception) { if (exception.getResponse().getStatus() == 404) - return new DNotFoundException("Service momentanément indisponible"); + return new DNotFoundException(trad.t("service.momentanement.indisponible")); if (exception.getResponse().getStatus() == 400) - return new DNotFoundException("Asso introuvable"); + return new DNotFoundException(trad.t("asso.introuvable")); } return throwable; }); diff --git a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java index 7128d0c..6315423 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/ClubEndpoints.java @@ -3,6 +3,7 @@ package fr.titionfire.ffsaf.rest; import fr.titionfire.ffsaf.data.model.ClubModel; import fr.titionfire.ffsaf.domain.service.ClubService; import fr.titionfire.ffsaf.domain.service.PDFService; +import fr.titionfire.ffsaf.domain.service.TradService; import fr.titionfire.ffsaf.domain.service.VirusScannerService; import fr.titionfire.ffsaf.net2.data.SimpleClubModel; import fr.titionfire.ffsaf.rest.data.*; @@ -53,6 +54,9 @@ public class ClubEndpoints { @ConfigProperty(name = "upload_dir") String media; + @Inject + TradService trad; + Consumer checkPerm = Unchecked.consumer(clubModel -> { if (!securityCtx.roleHas("federation_admin") && !securityCtx.isInClubGroup(clubModel.getId())) throw new DForbiddenException(); @@ -84,7 +88,7 @@ public class ClubEndpoints { @Operation(summary = "Renvoie les types de contacts pour les clubs", description = "Renvoie la liste des types de " + "contacts possibles pour les clubs") public Uni> getConcatType() { - return Uni.createFrom().item(Contact.toSite()); + return Uni.createFrom().item(Contact.toSite(trad)); } @GET @@ -125,7 +129,7 @@ public class ClubEndpoints { public Uni getById( @Parameter(description = "Identifiant de club") @PathParam("id") long id) { return clubService.getById(id).onItem().invoke(checkPerm).map(SimpleClub::fromModel) - .invoke(m -> m.setContactMap(Contact.toSite())); + .invoke(m -> m.setContactMap(Contact.toSite(trad))); } @PUT @@ -218,7 +222,7 @@ public class ClubEndpoints { }) public Uni getOfUser() { return clubService.getOfUser(securityCtx).map(SimpleClub::fromModel) - .invoke(m -> m.setContactMap(Contact.toSite())); + .invoke(m -> m.setContactMap(Contact.toSite(trad))); } @PUT diff --git a/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java b/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java index 7da843e..5d2c133 100644 --- a/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java +++ b/src/main/java/fr/titionfire/ffsaf/rest/data/MeData.java @@ -1,6 +1,7 @@ package fr.titionfire.ffsaf.rest.data; import fr.titionfire.ffsaf.data.model.MembreModel; +import fr.titionfire.ffsaf.domain.service.TradService; import fr.titionfire.ffsaf.utils.ResultPrivacy; import io.quarkus.runtime.annotations.RegisterForReflection; import lombok.Data; @@ -47,19 +48,19 @@ public class MeData { @Schema(description = "La liste des séléctions du membre.") private List selections; - public void setMembre(MembreModel membreModel) { + public void setMembre(MembreModel membreModel, TradService trad) { this.id = membreModel.getId(); this.lname = membreModel.getLname(); this.fname = membreModel.getFname(); - this.categorie = membreModel.getCategorie() == null ? "catégorie inconnue" : membreModel.getCategorie().getName(); - this.club = membreModel.getClub() == null ? "Sans club" : membreModel.getClub().getName(); - this.genre = membreModel.getGenre().str; + this.categorie = membreModel.getCategorie() == null ? trad.t("categorie.inconnue") : membreModel.getCategorie().getName(trad); + this.club = membreModel.getClub() == null ? trad.t("sans.club") : membreModel.getClub().getName(); + this.genre = membreModel.getGenre().getString(trad); this.licence = membreModel.getLicence(); this.country = membreModel.getCountry(); this.birth_date = membreModel.getBirth_date(); this.email = membreModel.getEmail(); - this.role = membreModel.getRole().str; - this.grade_arbitrage = membreModel.getGrade_arbitrage().str; + this.role = membreModel.getRole().getString(trad); + this.grade_arbitrage = membreModel.getGrade_arbitrage().getString(trad); this.resultPrivacy = membreModel.getResultPrivacy(); } } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Categorie.java b/src/main/java/fr/titionfire/ffsaf/utils/Categorie.java index a9c82be..77ac693 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Categorie.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Categorie.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.utils; +import fr.titionfire.ffsaf.domain.service.TradService; import io.quarkus.runtime.annotations.RegisterForReflection; import java.util.ResourceBundle; @@ -19,7 +20,7 @@ public enum Categorie { VETERAN2; public String getName(ResourceBundle BUNDLE) { - return switch (this){ + return switch (this) { case SUPER_MINI -> BUNDLE.getString("Cat.SUPER_MINI"); case MINI_POUSSIN -> BUNDLE.getString("Cat.MINI_POUSSIN"); case POUSSIN -> BUNDLE.getString("Cat.POUSSIN"); @@ -34,8 +35,24 @@ public enum Categorie { }; } + public String getName(TradService tradService) { + return switch (this) { + case SUPER_MINI -> tradService.translate("Cat.SUPER_MINI"); + case MINI_POUSSIN -> tradService.translate("Cat.MINI_POUSSIN"); + case POUSSIN -> tradService.translate("Cat.POUSSIN"); + case BENJAMIN -> tradService.translate("Cat.BENJAMIN"); + case MINIME -> tradService.translate("Cat.MINIME"); + case CADET -> tradService.translate("Cat.CADET"); + case JUNIOR -> tradService.translate("Cat.JUNIOR"); + case SENIOR1 -> tradService.translate("Cat.SENIOR1"); + case SENIOR2 -> tradService.translate("Cat.SENIOR2"); + case VETERAN1 -> tradService.translate("Cat.VETERAN1"); + case VETERAN2 -> tradService.translate("Cat.VETERAN2"); + }; + } + public String getName() { - return switch (this){ + return switch (this) { case SUPER_MINI -> "Super Mini"; case MINI_POUSSIN -> "Mini Poussin"; case POUSSIN -> "Poussin"; diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Contact.java b/src/main/java/fr/titionfire/ffsaf/utils/Contact.java index 2ff0a5e..69ed002 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Contact.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Contact.java @@ -1,32 +1,34 @@ package fr.titionfire.ffsaf.utils; +import fr.titionfire.ffsaf.domain.service.TradService; import io.quarkus.runtime.annotations.RegisterForReflection; import java.util.HashMap; @RegisterForReflection public enum Contact { - COURRIEL("Courriel"), - TELEPHONE("Téléphone"), - SITE("Site web"), - FACEBOOK("Facebook"), - INSTAGRAM("Instagram"), - AUTRE("Autre"); + COURRIEL, + TELEPHONE, + SITE, + FACEBOOK, + INSTAGRAM, + AUTRE; - public String name; - - Contact(String name) { - this.name = name; + public String getName(TradService trad) { + return switch (this) { + case COURRIEL -> trad.translate("Contact.COURRIEL"); + case TELEPHONE -> trad.translate("Contact.TELEPHONE"); + case SITE -> trad.translate("Contact.SITE"); + case FACEBOOK -> trad.translate("Contact.FACEBOOK"); + case INSTAGRAM -> trad.translate("Contact.INSTAGRAM"); + case AUTRE -> trad.translate("Contact.AUTRE"); + }; } - public void setName(String name) { - this.name = name; - } - - public static HashMap toSite() { + public static HashMap toSite(TradService trad) { HashMap map = new HashMap<>(); for (Contact contact : Contact.values()) { - map.put(contact.toString(), contact.name); + map.put(contact.toString(), contact.getName(trad)); } return map; } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/Genre.java b/src/main/java/fr/titionfire/ffsaf/utils/Genre.java index c6bd8e9..2ff8254 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/Genre.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/Genre.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.utils; +import fr.titionfire.ffsaf.domain.service.TradService; import io.quarkus.runtime.annotations.RegisterForReflection; @RegisterForReflection @@ -27,8 +28,17 @@ public enum Genre { } } + public String getString(TradService trad) { + return switch (this) { + case H -> trad.translate("Genre.Homme"); + case F -> trad.translate("Genre.Femme"); + case NA -> trad.translate("Genre.NA"); + }; + } + @Override public String toString() { return str; } + } diff --git a/src/main/java/fr/titionfire/ffsaf/utils/GradeArbitrage.java b/src/main/java/fr/titionfire/ffsaf/utils/GradeArbitrage.java index df9b927..faf2889 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/GradeArbitrage.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/GradeArbitrage.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.utils; +import fr.titionfire.ffsaf.domain.service.TradService; import io.quarkus.runtime.annotations.RegisterForReflection; @RegisterForReflection @@ -14,6 +15,14 @@ public enum GradeArbitrage { this.str = name; } + public String getString(TradService trad) { + return switch (this) { + case NA -> trad.translate("GradeArbitrage.NA"); + case ASSESSEUR -> trad.translate("GradeArbitrage.ASSESSEUR"); + case ARBITRE -> trad.translate("GradeArbitrage.ARBITRE"); + }; + } + @Override public String toString() { return str; diff --git a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java index b498495..63b8aa4 100644 --- a/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java +++ b/src/main/java/fr/titionfire/ffsaf/utils/RoleAsso.java @@ -1,5 +1,6 @@ package fr.titionfire.ffsaf.utils; +import fr.titionfire.ffsaf.domain.service.TradService; import io.quarkus.runtime.annotations.RegisterForReflection; @RegisterForReflection @@ -21,6 +22,19 @@ public enum RoleAsso { this.level = level; } + public String getString(TradService trad) { + return switch (this){ + case MEMBRE -> trad.translate("RoleAsso.MEMBRE"); + case PRESIDENT -> trad.translate("RoleAsso.PRESIDENT"); + case VPRESIDENT -> trad.translate("RoleAsso.VPRESIDENT"); + case SECRETAIRE -> trad.translate("RoleAsso.SECRETAIRE"); + case VSECRETAIRE -> trad.translate("RoleAsso.VSECRETAIRE"); + case TRESORIER -> trad.translate("RoleAsso.TRESORIER"); + case VTRESORIER -> trad.translate("RoleAsso.VTRESORIER"); + case MEMBREBUREAU -> trad.translate("RoleAsso.MEMBREBUREAU"); + }; + } + @Override public String toString() { return str; diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java index 202f538..aefc5b7 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCardboard.java @@ -5,6 +5,7 @@ import fr.titionfire.ffsaf.data.model.MatchModel; import fr.titionfire.ffsaf.data.repository.CardboardRepository; import fr.titionfire.ffsaf.data.repository.MatchRepository; import fr.titionfire.ffsaf.domain.entity.CardboardEntity; +import fr.titionfire.ffsaf.domain.service.TradService; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.ws.PermLevel; @@ -32,13 +33,16 @@ public class RCardboard { @Inject CardboardRepository cardboardRepository; + @Inject + TradService trad; + private Uni getById(long id, WebSocketConnection connection) { return matchRepository.findById(id) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException("Matche non trouver"); + throw new DNotFoundException(trad.t("matche.non.trouver")); if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException("Permission denied"); + throw new DForbiddenException(trad.t("permission.denied")); })); } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java index ecb9cde..73502ca 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RCategorie.java @@ -6,6 +6,7 @@ import fr.titionfire.ffsaf.data.model.TreeModel; import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.domain.entity.MatchEntity; import fr.titionfire.ffsaf.domain.entity.TreeEntity; +import fr.titionfire.ffsaf.domain.service.TradService; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.utils.TreeNode; @@ -46,13 +47,16 @@ public class RCategorie { @Inject CardboardRepository cardboardRepository; + @Inject + TradService trad; + private Uni getById(long id, WebSocketConnection connection) { return categoryRepository.findById(id) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException("Catégorie non trouver"); + throw new DNotFoundException(trad.t("categorie.non.trouver")); if (!o.getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException("Permission denied"); + throw new DForbiddenException(trad.t("permission.denied")); })); } diff --git a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java index a3437c8..a902ebe 100644 --- a/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java +++ b/src/main/java/fr/titionfire/ffsaf/ws/recv/RMatch.java @@ -4,6 +4,7 @@ import fr.titionfire.ffsaf.data.model.*; import fr.titionfire.ffsaf.data.repository.*; import fr.titionfire.ffsaf.domain.entity.MatchEntity; import fr.titionfire.ffsaf.domain.entity.TreeEntity; +import fr.titionfire.ffsaf.domain.service.TradService; import fr.titionfire.ffsaf.rest.exception.DForbiddenException; import fr.titionfire.ffsaf.rest.exception.DNotFoundException; import fr.titionfire.ffsaf.utils.ScoreEmbeddable; @@ -47,13 +48,16 @@ public class RMatch { @Inject CardboardRepository cardboardRepository; + @Inject + TradService trad; + private Uni getById(long id, WebSocketConnection connection) { return matchRepository.findById(id) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException("Matche non trouver"); + throw new DNotFoundException(trad.t("matche.non.trouver")); if (!o.getCategory().getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException("Permission denied"); + throw new DForbiddenException(trad.t("permission.denied")); })); } @@ -81,9 +85,9 @@ public class RMatch { return categoryRepository.findById(m.categorie) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException("Catégorie non trouver"); + throw new DNotFoundException(trad.t("categorie.non.trouver")); if (!o.getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException("Permission denied"); + throw new DForbiddenException(trad.t("permission.denied")); })) .chain(categoryModel -> creatMatch(categoryModel, m)) .chain(mm -> Panache.withTransaction(() -> matchRepository.create(mm))) @@ -294,9 +298,9 @@ public class RMatch { return categoryRepository.findById(data.categorie) .invoke(Unchecked.consumer(o -> { if (o == null) - throw new DNotFoundException("Catégorie non trouver"); + throw new DNotFoundException(trad.t("categorie.non.trouver")); if (!o.getCompet().getUuid().equals(connection.pathParam("uuid"))) - throw new DForbiddenException("Permission denied"); + throw new DForbiddenException(trad.t("permission.denied")); })) .call(cm -> data.matchesToRemove.isEmpty() ? Uni.createFrom().voidItem() : (Panache.withTransaction( diff --git a/src/main/resources/lang/String.properties b/src/main/resources/lang/String.properties deleted file mode 100644 index 7e87f97..0000000 --- a/src/main/resources/lang/String.properties +++ /dev/null @@ -1,16 +0,0 @@ -filtre.all=--tout-- -no.licence=Non licenci\u00E9 - - -# Categories -Cat.SUPER_MINI=Super Mini -Cat.MINI_POUSSIN=Mini Poussin -Cat.POUSSIN= Poussin -Cat.BENJAMIN=Benjamin -Cat.MINIME=Minime -Cat.CADET=Cadet -Cat.JUNIOR=Junior -Cat.SENIOR1=Senior 1 -Cat.SENIOR2=Senior 2 -Cat.VETERAN1=V\u00E9t\u00E9ran 1 -Cat.VETERAN2=V\u00E9t\u00E9ran 2 \ No newline at end of file diff --git a/src/main/resources/lang/messages_en.properties b/src/main/resources/lang/messages_en.properties new file mode 100644 index 0000000..262334f --- /dev/null +++ b/src/main/resources/lang/messages_en.properties @@ -0,0 +1,87 @@ +filtre.all=--all-- +no.licence=Not licensed +Cat.SUPER_MINI=Super Mini +Cat.MINI_POUSSIN=Mini Chick +Cat.POUSSIN=Chick +Cat.BENJAMIN=Benjamin +Cat.MINIME=Minime +Cat.CADET=Cadet +Cat.JUNIOR=Junior +Cat.SENIOR1=Senior 1 +Cat.SENIOR2=Senior 2 +Cat.VETERAN1=Veteran 1 +Cat.VETERAN2=Veteran 2 +access.denied=Access denied +club.not.found=Club not found +combattant.non.inscrit=Fighter not registered +comb.not.found=Fighter not found +matche.non.trouver=Match not found +permission.denied=Permission denied +categorie.non.trouver=Category not found + +RoleAsso.MEMBRE=Member +RoleAsso.PRESIDENT=President +RoleAsso.VPRESIDENT=Vice-President +RoleAsso.SECRETAIRE=Secretary +RoleAsso.VSECRETAIRE=Vice-Secretary +RoleAsso.TRESORIER=Treasurer +RoleAsso.VTRESORIER=Vice-Treasurer +RoleAsso.MEMBREBUREAU=Board Member + +sans.club=No club +categorie.inconnue=Unknown category +GradeArbitrage.NA=N/A +GradeArbitrage.ASSESSEUR=Assessor +GradeArbitrage.ARBITRE=Referee +Genre.Homme=Male +Genre.Femme=Female +Genre.NA=Not defined + +Contact.COURRIEL=Email +Contact.TELEPHONE=Phone +Contact.SITE=Website +Contact.FACEBOOK=Facebook +Contact.INSTAGRAM=Instagram +Contact.AUTRE=Other + +service.momentanement.indisponible=Service temporarily unavailable +asso.introuvable=Association not found +erreur.lors.calcul.du.trie=Error during sorting calculation +page.out.of.range=Page out of range +le.membre.appartient.pas.a.votre.club=Member no. %d does not belong to your club +email.deja.utilise.par=Email '%s' already used by %s %s +try.edit.licence=To register a new member, please leave the license field blank. (Unauthorized attempt to change the name on license %d for %s %s) +email.deja.utilise=Email already in use +regiter.new.membre=To register a new member, please use the dedicated button. +permission.insuffisante=Insufficient permission +membre.deja.existent=Member already exists +membre.rm.err1=Cannot delete a member who already has a license number +membre.rm.err2=Cannot delete a member with licenses +pas.de.licence.pour.la.saison.en.cours=No license for the current season +club.non.trouve=Club not found +pas.d.affiliation.pour.la.saison.en.cours=No affiliation for the current season +erreur.lors.de.la.selection.des.membres=Error during member selection +licence.rm.err1=Cannot delete a license for which payment is in progress +licence.deja.demandee=License already requested +impossible.de.supprimer.une.licence.deja.validee=Cannot delete an already validated license +impossible.de.supprimer.une.licence.deja.payee=Cannot delete an already paid license +vous.ne.pouvez.pas.creer.de.competition=You cannot create a competition +user.not.found=User %s not found +inscription.fermee=Registration closed +insc.err1=You do not have the right to register this member (by decision of the competition administrator) +insc.err2=You do not have the right to register (by decision of the competition administrator) +insc.err3=Modification blocked by the competition administrator +licence.non.trouve=License %s not found +nom.et.prenom.requis=Last name and first name required +combattant.non.trouve=Fighter %s %s not found +le.membre.n.existe.pas=Member %d does not exist +competition.is.not.internal=Competition is not INTERNAL +erreur.de.format.des.contacts=Contact format error +competition.not.found=Competition not found +saison.non.valid=Invalid season +demande.d.affiliation.deja.existante=Affiliation request already exists +affiliation.deja.existante=Affiliation already exists +licence.membre.n.1.inconnue=License member no. 1 unknown +licence.membre.n.2.inconnue=License member no. 2 unknown +licence.membre.n.3.inconnue=License member no. 3 unknown +demande.d.affiliation.non.trouve=Affiliation request not found diff --git a/src/main/resources/lang/messages_fr.properties b/src/main/resources/lang/messages_fr.properties new file mode 100644 index 0000000..54f6d55 --- /dev/null +++ b/src/main/resources/lang/messages_fr.properties @@ -0,0 +1,83 @@ +filtre.all=--Tous-- +no.licence=Non licencié +Cat.SUPER_MINI=Super Mini +Cat.MINI_POUSSIN=Mini-Poussin +Cat.POUSSIN=Poussin +Cat.BENJAMIN=Benjamin +Cat.MINIME=Minime +Cat.CADET=Cadet +Cat.JUNIOR=Junior +Cat.SENIOR1=Senior 1 +Cat.SENIOR2=Senior 2 +Cat.VETERAN1=Vétéran 1 +Cat.VETERAN2=Vétéran 2 +access.denied=Accès refusé +club.not.found=Club introuvable +combattant.non.inscrit=Combattant non inscrit +comb.not.found=Combat introuvable +matche.non.trouver=Match introuvable +permission.denied=Permission refusée +categorie.non.trouver=Catégorie introuvable +RoleAsso.MEMBRE=Membre +RoleAsso.PRESIDENT=Président +RoleAsso.VPRESIDENT=Vice-Président +RoleAsso.SECRETAIRE=Secrétaire +RoleAsso.VSECRETAIRE=Vice-Secrétaire +RoleAsso.TRESORIER=Trésorier +RoleAsso.VTRESORIER=Vice-Trésorier +RoleAsso.MEMBREBUREAU=Membre bureau +sans.club=Sans club +categorie.inconnue=catégorie inconnue +GradeArbitrage.NA=N/A +GradeArbitrage.ASSESSEUR=Assesseur +GradeArbitrage.ARBITRE=Arbitre +Genre.Homme=Homme +Genre.Femme=Femme +Genre.NA=Non défini +Contact.COURRIEL=Courriel +Contact.TELEPHONE=Téléphone +Contact.SITE=Site web +Contact.FACEBOOK=Facebook +Contact.INSTAGRAM=Instagram +Contact.AUTRE=Autre +service.momentanement.indisponible=Service momentanément indisponible +asso.introuvable=Association introuvable +erreur.lors.calcul.du.trie=Erreur lors du calcul du tri +page.out.of.range=Page out of range +le.membre.appartient.pas.a.votre.club=Le membre n°%d n?appartient pas à votre club +email.deja.utilise.par=L?adresse e-mail '%s' est déjà utilisée par %s %s +try.edit.licence=Pour enregistrer un nouveau membre, veuillez laisser le champ licence vide. (Tentative de modification non autorisée du nom sur la licence %d pour %s %s) +email.deja.utilise=Adresse e-mail déjà utilisée +regiter.new.membre=Pour enregistrer un nouveau membre, veuillez utiliser le bouton prévu à cet effet. +permission.insuffisante=Permissions insuffisantes +membre.deja.existent=Membre déjà existant +membre.rm.err1=Impossible de supprimer un membre qui possède déjà un numéro de licence +membre.rm.err2=Impossible de supprimer un membre ayant des licences actives +pas.de.licence.pour.la.saison.en.cours=Aucune licence pour la saison en cours +club.non.trouve=Club introuvable +pas.d.affiliation.pour.la.saison.en.cours=Aucune affiliation pour la saison en cours +erreur.lors.de.la.selection.des.membres=Erreur lors de la sélection des membres +licence.rm.err1=Impossible de supprimer une licence pour laquelle un paiement est en cours +licence.deja.demandee=Licence déjà demandée +impossible.de.supprimer.une.licence.deja.validee=Impossible de supprimer une licence déjà validée +impossible.de.supprimer.une.licence.deja.payee=Impossible de supprimer une licence déjà payée +vous.ne.pouvez.pas.creer.de.competition=Vous n?êtes pas autorisé à créer une compétition +user.not.found=Utilisateur %s introuvable +inscription.fermee=Inscription fermée +insc.err1=Vous n?êtes pas autorisé à inscrire ce membre (décision de l?administrateur de la compétition) +insc.err2=Vous n?êtes pas autorisé à vous inscrire (décision de l?administrateur de la compétition) +insc.err3=Modification bloquée par l?administrateur de la compétition +licence.non.trouve=Licence %s introuvable +nom.et.prenom.requis=Nom et prénom obligatoires +combattant.non.trouve=Combattant %s %s introuvable +le.membre.n.existe.pas=Le membre n°%d n?existe pas +competition.is.not.internal=Competition is not INTERNAL +erreur.de.format.des.contacts=Format des contacts invalide +competition.not.found=Compétition introuvable +saison.non.valid=Saison invalide +demande.d.affiliation.deja.existante=Une demande d?affiliation existe déjà +affiliation.deja.existante=Affiliation déjà existante +licence.membre.n.1.inconnue=Licence du membre n°1 inconnue +licence.membre.n.2.inconnue=Licence du membre n°2 inconnue +licence.membre.n.3.inconnue=Licence du membre n°3 inconnue +demande.d.affiliation.non.trouve=Demande d?affiliation introuvable \ No newline at end of file diff --git a/src/main/webapp/package-lock.json b/src/main/webapp/package-lock.json index c8d23ff..43a9829 100644 --- a/src/main/webapp/package-lock.json +++ b/src/main/webapp/package-lock.json @@ -18,12 +18,16 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "i18next": "^25.7.4", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", "leaflet": "^1.9.4", "obs-websocket-js": "^5.0.7", "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^16.5.2", "react-is": "^19.0.0", "react-leaflet": "^4.2.1", "react-loader-spinner": "^6.1.6", @@ -345,12 +349,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", - "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "engines": { "node": ">=6.9.0" } @@ -1871,6 +1872,14 @@ "node": ">=0.8" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2990,6 +2999,60 @@ "node": ">= 0.4" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.7.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz", + "integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", + "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/ignore": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", @@ -3634,6 +3697,25 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -4028,6 +4110,32 @@ "react": "^18.2.0" } }, + "node_modules/react-i18next": { + "version": "16.5.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-16.5.2.tgz", + "integrity": "sha512-GG/SBVxx9dvrO1uCs8VYdKfOP8NEBUhNP+2VDQLCifRJ8DL1qPq296k2ACNGyZMDe7iyIlz/LMJTQOs8HXSRvw==", + "dependencies": { + "@babel/runtime": "^7.28.4", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 25.6.2", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", @@ -4221,11 +4329,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -4704,6 +4807,11 @@ "node": ">=4" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tslib": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", @@ -4852,6 +4960,14 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4938,6 +5054,28 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/main/webapp/package.json b/src/main/webapp/package.json index 418755f..469cbf3 100644 --- a/src/main/webapp/package.json +++ b/src/main/webapp/package.json @@ -20,12 +20,16 @@ "@fortawesome/react-fontawesome": "^0.2.0", "axios": "^1.6.5", "browser-image-compression": "^2.0.2", + "i18next": "^25.7.4", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", "jszip": "^3.10.1", "leaflet": "^1.9.4", "obs-websocket-js": "^5.0.7", "proj4": "^2.11.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-i18next": "^16.5.2", "react-is": "^19.0.0", "react-leaflet": "^4.2.1", "react-loader-spinner": "^6.1.6", diff --git a/src/main/webapp/public/competition.js b/src/main/webapp/public/competition.js index c8e7352..e6cdd59 100644 --- a/src/main/webapp/public/competition.js +++ b/src/main/webapp/public/competition.js @@ -1,12 +1,14 @@ +import i18next from 'https://cdn.jsdelivr.net/npm/i18next@25.7.4/+esm'; +import i18nextHttpBackend from 'https://cdn.jsdelivr.net/npm/i18next-http-backend@3.0.2/+esm' +import i18nextBrowserLanguagedetector from 'https://cdn.jsdelivr.net/npm/i18next-browser-languagedetector@8.2.0/+esm' + let apiUrlRoot = ""; const rootDiv = document.getElementById("safca_api_data"); - -const header = `

Résultat de la compétition :

` -const backButton = `Retour` const cupImg = `` + const voidFunction = () => { } let lastRf = 0; @@ -41,18 +43,23 @@ function setSubPage(name) { } function homePage() { - rootDiv.innerHTML = header; + rootDiv.innerHTML = `

${i18next.t('résultatDeLaCompétition')} :

`; let content = document.createElement('div'); content.innerHTML = ` ` rootDiv.append(content) + + document.getElementById('pouleLink').addEventListener('click', () => setSubPage('poule')); + document.getElementById('combLink').addEventListener('click', () => setSubPage('comb')); + document.getElementById('clubLink').addEventListener('click', () => setSubPage('club')); + document.getElementById('allLink').addEventListener('click', () => setSubPage('all')); } let loadingAnimationStep = 0; @@ -63,7 +70,7 @@ function startLoading(root) { element.id = id; const anim = setInterval(() => { - let str = "Chargement"; + let str = i18next.t('chargement'); for (let i = 0; i < loadingAnimationStep; i++) { str += "."; } @@ -84,11 +91,11 @@ function scoreToString(score) { const scorePrint = (s1) => { switch (s1) { case -997: - return "disc."; + return i18next.t('disc.'); case -998: - return "abs."; + return i18next.t('abs.'); case -999: - return "for."; + return i18next.t('for.'); case -1000: return ""; default: @@ -111,11 +118,11 @@ function dateToString(date) { let d = Math.floor((current - date_2) / (1000 * 60 * 60 * 24)); if (d === 0) - return "Aujourd'hui à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + return i18next.t('aujourdhuià', {time: date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"})}); else if (d === 1) - return "Hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + return i18next.t('hierà', {time: date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"})}); else if (d === 2) - return "Avant-hier à " + date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"}); + return i18next.t('avanthierà', {time: date_.toLocaleTimeString([], {hour: "2-digit", minute: "2-digit"})}); else return date_.toLocaleDateString(); } @@ -175,11 +182,11 @@ function buildPouleMenu(isPoule, change_view) { return li; } - const li1 = createTab('Poule', isPoule, function () { + const li1 = createTab(i18next.t('poule'), isPoule, function () { change_view(true); }); ul.appendChild(li1); - const li2 = createTab('Tournois', !isPoule, function () { + const li2 = createTab(i18next.t('tournois'), !isPoule, function () { change_view(false); }); ul.appendChild(li2); @@ -195,12 +202,12 @@ function buildMatchArray(matchs) {
- + - + - - + + ` for (const match of matchs) { @@ -225,12 +232,12 @@ function buildRankArray(rankArray) {
Rouge${i18next.t('rouge')} Scores${i18next.t('scores')} BleuDate${i18next.t('bleu')}${i18next.t('date')}
- - - - - - + + + + + + ` for (const row of rankArray) { @@ -255,10 +262,12 @@ function buildTree(treeData) { } function poulePage(location) { - rootDiv.innerHTML = header + backButton; + rootDiv.innerHTML = `

${i18next.t('résultatDeLaCompétition')} :

${i18next.t('back')}`; + document.getElementById('homeLink').addEventListener('click', () => setSubPage('home')); + const content = document.createElement('div'); content.style.marginTop = '1em'; - content.innerHTML = '

Recherche par catégorie

'; + content.innerHTML = `

${i18next.t('rechercheParCatégorie')}

`; const dataContainer = document.createElement('div'); dataContainer.id = 'data-container'; @@ -286,7 +295,7 @@ function poulePage(location) { for (const g in poule.matchs) { if (Object.keys(poule.matchs).length > 1) { const text = document.createElement('h4'); - text.textContent = `Groupe ${g}`; + text.textContent = `${i18next.t('poule')} ${g}`; text.style.marginTop = '2em'; dataContainer.append(text); } @@ -303,7 +312,7 @@ function poulePage(location) { for (const g in poule.matchs) { if (Object.keys(poule.matchs).length > 1) { const text = document.createElement('h4'); - text.textContent = `Groupe ${g}`; + text.textContent = `${i18next.t('poule')} ${g}`; text.style.marginTop = '2em'; dataContainer.append(text); } @@ -323,7 +332,7 @@ function poulePage(location) { }) .catch(e => { console.error(e); - dataContainer.replaceChildren(new Text("Erreur de chargement de la poule")); + dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDeLaPoule'))); }) .finally(() => stopLoading(loading)); } @@ -334,7 +343,7 @@ function poulePage(location) { .then(poule => { const select = document.createElement('select'); select.setAttribute('id', poule.id); - select.innerHTML = ``; + select.innerHTML = ``; for (const pouleKey of Object.keys(poule).sort((a, b) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase()))) { select.innerHTML += ``; } @@ -356,7 +365,7 @@ function poulePage(location) { loadPoule(); } }) - .catch(() => rootDiv.append(new Text("Erreur de chargement des catégories"))) + .catch(() => rootDiv.append(new Text(i18next.t('erreurDeChargementDesCatégories')))) .finally(() => stopLoading(loading)); rfFonction = () => { @@ -372,28 +381,33 @@ function buildCombView(comb) { let arrayContent = `

Info :

    -
  • Nom Prénom : ${comb.name}
  • -
  • Club : ${comb.club}
  • -
  • Catégorie : ${comb.cat}
  • +
  • ${i18next.t('nomPrénom')} : ${comb.name}
  • +
  • ${i18next.t('club')} : ${comb.club}
  • +
  • ${i18next.t('catégorie')} : ${comb.cat}
-

Statistique :

+

${i18next.t('statistique')} :

    -
  • Taux de victoire : ${comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0)}% (${comb.totalWin} sur ${comb.matchs.length})
  • -
  • Points marqués : ${comb.pointMake}
  • -
  • Points reçus : ${comb.pointTake}
  • -
  • Ratio du score (point marqué / point reçu): ${comb.pointRatio.toFixed(3)}
  • +
  • ${i18next.t('tauxDeVictoire2', { + nb: comb.matchs.length === 0 ? "---" : (comb.totalWin / comb.matchs.length * 100).toFixed(0), + victoires: comb.totalWin, + matchs: comb.matchs.length + })} +
  • +
  • ${i18next.t('pointsMarqués2', {nb: comb.pointMake})}
  • +
  • ${i18next.t('pointsReçus2', {nb: comb.pointTake})}
  • +
  • ${i18next.t('ratioDuScore2', {nb: comb.pointRatio.toFixed(3)})}
-

Liste des matchs:

+

${i18next.t('listeDesMatchs')}:

PlaceNomVictoireRatioPoints marquésPoints reçus${i18next.t('place')}${i18next.t('nom')}${i18next.t('victoire')}${i18next.t('ratio')}${i18next.t('pointsMarqués')}${i18next.t('pointsReçus')}
- - - - - + + + + + ` @@ -414,10 +428,12 @@ function buildCombView(comb) { } function combPage(location) { - rootDiv.innerHTML = header + backButton; + rootDiv.innerHTML = `

${i18next.t('résultatDeLaCompétition')} :

${i18next.t('back')}`; + document.getElementById('homeLink').addEventListener('click', () => setSubPage('home')); + const content = document.createElement('div'); content.style.marginTop = '1em'; - content.innerHTML = '

Recherche par combattant

'; + content.innerHTML = `

${i18next.t('rechercheParCombattant')}

`; const dataContainer = document.createElement('div'); dataContainer.id = 'data-container'; @@ -430,7 +446,7 @@ function combPage(location) { console.log(comb); dataContainer.replaceChildren(buildCombView(comb)); }) - .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du combattant"))) + .catch(() => dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDuCombattant')))) .finally(() => stopLoading(loading)); } @@ -439,7 +455,7 @@ function combPage(location) { .then(response => response.json()) .then(combs => { const select = document.createElement('select'); - select.innerHTML = ``; + select.innerHTML = ``; for (const comb of Object.keys(combs).sort()) { select.innerHTML += ``; } @@ -458,7 +474,7 @@ function combPage(location) { loadComb(tmp); } }) - .catch(() => rootDiv.append(new Text("Erreur de chargement des combattants"))) + .catch(() => rootDiv.append(new Text(i18next.t('erreurDeChargementDesCombattants')))) .finally(() => stopLoading(loading)); rootDiv.append(content) @@ -467,34 +483,34 @@ function combPage(location) { function buildClubView(club) { const pouleDiv = document.createElement('div'); let arrayContent = ` -

Info :

+

${i18next.t('info')} :

    -
  • Nom : ${club.name}
  • -
  • Nombre d'inscris : ${club.nb_insc}
  • +
  • ${i18next.t('nom')} : ${club.name}
  • +
  • ${i18next.t('nombreDinscris')} : ${club.nb_insc}
-

Statistique :

+

${i18next.t('statistique')} :

    -
  • Nombre de match disputé : ${club.nb_match}
  • -
  • Nombre de victoires : ${club.match_w}
  • -
  • Ratio de victoires moyen : ${club.ratioVictoire.toFixed(3)}
  • -
  • Points marqués : ${club.pointMake}
  • -
  • Points reçus : ${club.pointTake}
  • -
  • Ratio de points moyen : ${club.ratioPoint.toFixed(3)}
  • +
  • ${i18next.t('nombreDeMatchDisputé2', {nb: club.nb_match})}
  • +
  • ${i18next.t('nombreDeVictoires2', {nb: club.match_w})}
  • +
  • ${i18next.t('ratioDeVictoiresMoyen2', {nb: club.ratioVictoire.toFixed(3)})}
  • +
  • ${i18next.t('pointsMarqués2', {nb: club.pointMake})}
  • +
  • ${i18next.t('pointsReçus2', {nb: club.pointTake})}
  • +
  • ${i18next.t('ratioDePointsMoyen2', {nb: club.ratioPoint.toFixed(3)})}
-

Liste des menbres :

+

${i18next.t('listeDesMenbres')} :

DatePouleAdversaireScoresRatio${i18next.t('date')}${i18next.t('poule')}${i18next.t('adversaire')}${i18next.t('scores')}${i18next.t('ratio')}
- - - - - - - - + + + + + + + + ` for (const comb of club.combs) { @@ -516,10 +532,12 @@ function buildClubView(club) { } function clubPage(location) { - rootDiv.innerHTML = header + backButton; + rootDiv.innerHTML = `

${i18next.t('résultatDeLaCompétition')} :

${i18next.t('back')}`; + document.getElementById('homeLink').addEventListener('click', () => setSubPage('home')); + const content = document.createElement('div'); content.style.marginTop = '1em'; - content.innerHTML = '

Recherche par club

'; + content.innerHTML = `

${i18next.t('rechercheParClub')}

`; const dataContainer = document.createElement('div'); dataContainer.id = 'data-container'; @@ -532,7 +550,7 @@ function clubPage(location) { console.log(club); dataContainer.replaceChildren(buildClubView(club)); }) - .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement du club"))) + .catch(() => dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDuClub')))) .finally(() => stopLoading(loading)); } @@ -541,7 +559,7 @@ function clubPage(location) { .then(response => response.json()) .then(clubs => { const select = document.createElement('select'); - select.innerHTML = ``; + select.innerHTML = ``; for (const club of Object.keys(clubs).sort()) { select.innerHTML += ``; } @@ -562,7 +580,7 @@ function clubPage(location) { loadComb(tmp); } }) - .catch(() => rootDiv.append(new Text("Erreur de chargement des clubs"))) + .catch(() => rootDiv.append(new Text(i18next.t('erreurDeChargementDesClubs')))) .finally(() => stopLoading(loading)); rootDiv.append(content) @@ -571,27 +589,27 @@ function clubPage(location) { function buildCombsView(combs) { const pouleDiv = document.createElement('div'); let arrayContent = ` -

Statistique :

+

${i18next.t('statistique')} :

    -
  • Nombre d'inscris : ${combs.nb_insc}
  • -
  • Nombre de match disputé : ${combs.tt_match}
  • -
  • Points marqués : ${combs.point}
  • +
  • ${i18next.t('nombreDinscris2', {nb: combs.nb_insc})}
  • +
  • ${i18next.t('nombreDeMatchDisputé2', {nb: combs.tt_match})}
  • +
  • ${i18next.t('pointsMarqués2', {nb: combs.point})}
-

Liste des combattants :

+

${i18next.t('listeDesCombattants')} :

CatégorieNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatio points${i18next.t('catégorie')}${i18next.t('nom')}${i18next.t('victoires')}${i18next.t('défaites')}${i18next.t('ratioVictoires')}${i18next.t('pointsMarqués')}${i18next.t('pointsReçus')}${i18next.t('ratioPoints')}
- - - - - - - - - + + + + + + + + + ` for (const comb of combs.combs) { @@ -614,7 +632,9 @@ function buildCombsView(combs) { } function combsPage() { - rootDiv.innerHTML = header + backButton; + rootDiv.innerHTML = `

${i18next.t('résultatDeLaCompétition')} :

${i18next.t('back')}`; + document.getElementById('homeLink').addEventListener('click', () => setSubPage('home')); + const content = document.createElement('div'); content.style.marginTop = '1em'; @@ -628,26 +648,44 @@ function combsPage() { console.log(combs); dataContainer.replaceChildren(buildCombsView(combs)); }) - .catch(() => dataContainer.replaceChildren(new Text("Erreur de chargement de la liste"))) + .catch(() => dataContainer.replaceChildren(new Text(i18next.t('erreurDeChargementDeLaListe')))) .finally(() => stopLoading(loading)); content.append(dataContainer); rootDiv.append(content) } -window.addEventListener("load", () => { - let path = document.getElementById('safca_api_script').src; - const urlParams = new URLSearchParams(new URL(path).search); - apiUrlRoot = path.substring(0, path.lastIndexOf('/')) + "/api/public/result/" + urlParams.get("id"); +export async function initCompetitionApi(apiUrlRoot_) { + apiUrlRoot = apiUrlRoot_; + + const options = { + order: ['querystring', 'cookie', 'localStorage', 'sessionStorage', 'navigator', 'htmlTag'], + caches: [], + } + + await i18next + .use(i18nextHttpBackend) + .use(i18nextBrowserLanguagedetector) + .init({ + fallbackLng: 'fr', + debug: true, + interpolation: { + escapeValue: true, + }, + detection: options, + ns: ['result'], + defaultNS: 'result', + }) console.log("apiUrlRoot:", apiUrlRoot) + console.log("FFSAF Competition API initialized.") let hash = window.location.hash.substring(1); if (hash.length === 0) setSubPage('home'); else setSubPage(hash); -}); +} class TreeNode { constructor(data) { @@ -671,7 +709,7 @@ class TreeNode { } function initTree(data_in) { - out = []; + let out = []; for (const din of data_in) { out.push(parseTree(din)); } @@ -818,7 +856,7 @@ function drawGraph(root = []) { ctx.textBaseline = 'top'; for (let i = 0; i < scores.length; i++) { - const score = scores[i].s1+"-"+scores[i].s2; + const score = scores[i].s1 + "-" + scores[i].s2; const div = (scores.length <= 2) ? 2 : (scores.length >= 4) ? 4 : 3; const text = ctx.measureText(score); let dx = (size * 2 - text.width) / 2; diff --git a/src/main/webapp/public/locales/en/cm.json b/src/main/webapp/public/locales/en/cm.json new file mode 100644 index 0000000..b21846e --- /dev/null +++ b/src/main/webapp/public/locales/en/cm.json @@ -0,0 +1,143 @@ +{ + "--SélectionnerUnCombattant--": "-- Select a fighter --", + "--Tous--": "-- All --", + "actuel": "Current", + "administration": "Administration", + "adresseDuServeur": "Server address", + "ajouter": "Add", + "ajouterDesCombattants": "Add fighters", + "attention": "Warning", + "aucuneConfigurationObs": "No OBS configuration found, please import one", + "bleu": "Blue", + "blue": "Blue", + "catégorie": "Category", + "chrono.+/-...S": "+/- ... s", + "chrono.+10S": "+10 s", + "chrono.+1S": "+1 s", + "chrono.-10S": "-10 s", + "chrono.-1S": "-1 s", + "chrono.arrêter": "Stop", + "chrono.définirLeTemps": "Set time", + "chrono.démarrer": "Start", + "chrono.editionTemps": "Time editing", + "chrono.entrezLeTempsEnS": "Enter time in seconds", + "chrono.recapTemps": "Time: {{temps}}, pause: {{pause}}", + "chronomètre": "Stopwatch", + "club": "Club", + "compétition": "Competition", + "compétitionManager": "Competition manager", + "config.obs.dossierDesResources": "Resources folder", + "config.obs.motDePasseDuServeur": "Server password", + "config.obs.warn1": "/! The password will be stored in plain text; it is recommended to use it only on OBS WebSocket and to change it between each competition", + "config.obs.ws": "ws://", + "configurationObs": "OBS Configuration", + "confirm1": "This match already has results; are you sure you want to delete it?", + "confirm2.msg": "Do you really want to change the tournament tree size or the loser matches? This will modify existing matches (including possible deletions)!", + "confirm2.title": "Tournament tree change", + "confirm3.msg": "Do you really want to remove the {{typeStr}} part from the category? This will delete the matches contained in this part!", + "confirm3.title": "Change category type", + "confirm4.msg": "Do you really want to delete the category {{name}}. This will delete all associated matches!", + "confirm4.title": "Delete category", + "conserverUniquementLesMatchsTerminés": "Keep only finished matches", + "contre": "vs", + "créerLesMatchs": "Create matches", + "demi-finalesEtFinales": "Semi-finals and finals", + "duréePause": "Pause duration", + "duréeRound": "Round duration", + "editionDeLaCatégorie": "Edit category", + "enregister": "Save", + "enregistrer": "Save", + "epéeBouclier": "Sword and shield", + "err1": "A fighter cannot fight themselves!", + "err2": "The combat zone name format is invalid. Please separate names with ';'.", + "err3": "At least one type (pool or tournament) must be selected.", + "erreurLorsDeLaCopieDansLePresse": "Error while copying to clipboard: ", + "erreurLorsDeLaCréationDesMatchs": "Error while creating matches: ", + "exporter": "Export", + "fermer": "Close", + "finalesUniquement": "Finals only", + "genre": "Gender", + "genre.f": "F", + "genre.h": "M", + "genre.na": "NA", + "inscrit": "Registered", + "manche": "Round", + "matchPourLesPerdantsDuTournoi": "Match for tournament losers:", + "matches": "Matches", + "modifier": "Edit", + "msg1": "There are already matches in this pool; what do you want to do with them?", + "neRienConserver": "Keep nothing", + "no": "No.", + "nom": "Name", + "nomDesZonesDeCombat": "Combat zone names <1>(separated by ';')", + "nouvelle...": "New...", + "obs.préfixDesSources": "Source prefix", + "pays": "Country", + "poids": "Weight", + "poule": "Pool", + "poulePour": "Pool for: ", + "préparation...": "Preparing...", + "rouge": "Red", + "réinitialiser": "Reset", + "résultat": "Result", + "sansPoule": "No pool", + "sauvegarde": "Backup", + "sauvegarder": "Save", + "score": "Score", + "score.err1": "Cannot end a tournament match in a draw.", + "score.spe": "Special scores:
-997: disqualified
-998: absent
-999: forfeit", + "scores": "Scores", + "secrétariatsDeLice": "Ring secretariats", + "select.aucunCombattantDisponible": "No fighters available", + "select.aucunCombattantSélectionné": "No fighters selected", + "select.msg1": "(0 = disabled)", + "select.recherche": "Search", + "select.sélectionnerDesCombatants": "Select fighters", + "select.à": "to", + "serveur": "Server", + "suivant": "Next", + "supprimer": "Delete", + "sélectionneLesModesDaffichage": "Select display modes", + "sélectionner": "Select", + "terminé": "Finished", + "texteCopiéDansLePresse": "Text copied to clipboard! Paste it into an HTML tag on your WordPress.", + "toast.createCategory.error": "Error while creating the category", + "toast.createCategory.pending": "Creating category...", + "toast.createCategory.success": "Category created!", + "toast.deleteCategory.error": "Error while deleting the category", + "toast.deleteCategory.pending": "Deleting category...", + "toast.deleteCategory.success": "Category deleted!", + "toast.matchs.create.error": "Error while creating matches.", + "toast.matchs.create.pending": "Creating matches in progress...", + "toast.matchs.create.success": "Matches created successfully.", + "toast.updateCategory.error": "Error while updating the category", + "toast.updateCategory.pending": "Updating category...", + "toast.updateCategory.success": "Category updated!", + "toast.updateMatchScore.error": "Error while updating match score", + "toast.updateMatchScore.pending": "Updating match score...", + "toast.updateMatchScore.success": "Match score updated!", + "toast.updateTrees.error": "Error while updating trees", + "toast.updateTrees.init.error": "Error while creating trees", + "toast.updateTrees.init.pending": "Creating tournament trees...", + "toast.updateTrees.init.success": "Trees created!", + "toast.updateTrees.pending": "Updating tournament trees...", + "toast.updateTrees.success": "Trees updated!", + "tournoi": "Tournament", + "tournois": "Tournaments", + "tousLesMatchs": "All matches", + "toutConserver": "Keep all", + "ttm.admin.obs": "Short click: Download resources. Long click: Create OBS configuration", + "ttm.admin.scripte": "Copy integration script", + "ttm.table.inverserLaPosition": "Reverse fighter positions on this screen", + "ttm.table.obs": "Short click: Load configuration and connect. Long click: Ring configuration", + "ttm.table.pub_aff": "Open public display", + "ttm.table.pub_score": "Show scores on public display", + "type": "Type", + "téléchargement": "Download", + "téléchargementEnCours": "Downloading...", + "téléchargementTerminé!": "Download completed!", + "uneCatégorie": "a category", + "valider": "Validate", + "zone": "Zone", + "zoneDeCombat": "Combat zone" +} diff --git a/src/main/webapp/public/locales/en/common.json b/src/main/webapp/public/locales/en/common.json new file mode 100644 index 0000000..50fe38a --- /dev/null +++ b/src/main/webapp/public/locales/en/common.json @@ -0,0 +1,543 @@ +{ + "(optionnelle)": "(optional)", + "---SansClub---": "--- no club ---", + "---ToutLesClubs---": "--- all clubs ---", + "---ToutLesPays---": "--- all countries ---", + "---TouteLesCatégories---": "--- all categories ---", + "--NonLicencier--": "-- Not licensed --", + "--SélectionnerCatégorie--": "-- Select category --", + "1Catégorie": "+1 category", + "2Catégorie": "+2 categories", + "activer": "Activate", + "admin": "Administration", + "administrateur": "Administrator", + "adresse": "Address", + "adresseAdministrative": "Administrative address", + "aff.ancienNom": "Former name: {{name}}", + "aff.byMembreSim": "By similar member", + "aff.byNewMenbre": "By new member", + "aff.byNoLicence": "By license number", + "aff.info1": "This club has already been affiliated (affiliation no. {{no}})", + "aff.membreNo": "Member no. {{no}}", + "aff.nomDuClub": "Club name", + "aff.raisonDuRefus": "Reason for refusal", + "aff.raisonDuRefus.msg": "Please indicate the reason for refusal", + "aff.refusConfirm": "Are you sure you want to refuse this request?", + "aff.refuserLaDemande": "Refuse the request", + "aff.refuserLaDemande.detail": "Are you sure you want to refuse this request?", + "aff.submit.error1": "Please enter a valid license number for member {{id}}", + "aff.submit.error2": "Please enter a valid license number for member {{id}}", + "aff.submit.error3": "Please enter a valid email for member {{id}}", + "aff.toast.accept.error": "Failed to accept affiliation", + "aff.toast.accept.pending": "Accepting affiliation in progress", + "aff.toast.accept.success": "Affiliation accepted successfully 🎉", + "aff.toast.del.error": "Failed to delete affiliation request", + "aff.toast.del.pending": "Deleting affiliation request in progress", + "aff.toast.del.success": "Affiliation request deleted successfully 🎉", + "aff.toast.del2.error": "Failed to delete affiliation", + "aff.toast.del2.pending": "Deleting affiliation in progress", + "aff.toast.del2.success": "Affiliation deleted successfully 🎉", + "aff.toast.save.error": "Failed to save affiliation request", + "aff.toast.save.pending": "Saving affiliation request in progress", + "aff.toast.save.success": "Affiliation request saved successfully 🎉", + "aff.toast.save2.error": "Failed to save affiliation", + "aff.toast.save2.pending": "Saving affiliation in progress", + "aff.toast.save2.success": "Affiliation saved successfully 🎉", + "aff_req.appuyerSurRechercher": "Press search to complete", + "aff_req.association": "The association", + "aff_req.button.cancel": "Cancel my request", + "aff_req.button.confirm": "Confirm my affiliation request", + "aff_req.button.save": "Save changes", + "aff_req.denomination": "Denomination", + "aff_req.disposeLicence": "Already has a license", + "aff_req.error1": "The role of member {{i}} is required", + "aff_req.error2": "The SIRET/RNA format is invalid", + "aff_req.nomDeLassociation": "Association name", + "aff_req.text1": "Affiliation is annual and valid for one sports season: from September 1st to August 31st of the following year.", + "aff_req.text2": "To affiliate, a sports association must meet the following conditions:", + "aff_req.text2.li": [ + "Have its headquarters in France or the Principality of Monaco", + "Be constituted in accordance with Chapter 1 of Title II of Book 1 of the Sports Code", + "Pursue a purpose that falls within the definition of Article 1 of the Federation's statutes", + "Have statutes compatible with the principles of organization and operation of the Federation", + "Ensure freedom of opinion and respect for the rights of defense within it, and prohibit any discrimination", + "Comply with the rules of supervision, hygiene, and safety established by the Federation's regulations" + ], + "aff_req.text3": "After validation of your request, you will receive a temporary identifier and password to access your FFSAF space", + "aff_req.text4": "Note that to finalize your affiliation, you will need to:", + "aff_req.text4.li1": "Have at least three licensed members, including the president", + "aff_req.text4.li2": "Have paid the fees provided for by the federal regulations", + "aff_req.text5": "You can later add publicly visible addresses for your training locations", + "aff_req.text6": "Leave blank to make no changes. (If a coat of arms has already been sent with this request, it will be used; otherwise, we will use the one from the previous affiliation)", + "aff_req.text7": "Affiliation request sent successfully", + "aff_req.text8": "Once your request is validated, you will receive a temporary identifier and password to access your FFSAF space", + "aff_req.toast.undo.error": "Failed to cancel affiliation request", + "aff_req.toast.undo.pending": "Cancelling affiliation request in progress", + "aff_req.toast.undo.success": "Affiliation request cancelled successfully 🎉", + "afficherLétatDesAffiliation": "Display affiliation status", + "affiliation": "Affiliation", + "affiliationNo": "Affiliation no. {{no}}", + "ajout": "Addition", + "ajouterUnClub": "Add a club", + "ajouterUnMembre": "Add a member", + "all_season": "--- all seasons ---", + "au": "to", + "aucun": "None", + "aucunMembreSélectionné": "No member selected", + "back": "« back", + "blason": "Coat of arms", + "bureau": "Board", + "button.accepter": "Accept", + "button.ajouter": "Add", + "button.annuler": "Cancel", + "button.appliquer": "Apply", + "button.confirmer": "Confirm", + "button.créer": "Create", + "button.enregister": "Save", + "button.enregistrer": "Save", + "button.fermer": "Close", + "button.modifier": "Edit", + "button.refuser": "Refuse", + "button.seDésinscrire": "Unsubscribe", + "button.suivant": "Next", + "button.supprimer": "Delete", + "cat.benjamin": "Benjamin", + "cat.cadet": "Cadet", + "cat.catégorieInconnue": "Unknown category", + "cat.junior": "Junior", + "cat.miniPoussin": "Mini Chick", + "cat.minime": "Minime", + "cat.poussin": "Chick", + "cat.senior1": "Senior 1", + "cat.senior2": "Senior 2", + "cat.superMini": "Super Mini", + "cat.vétéran1": "Veteran 1", + "cat.vétéran2": "Veteran 2", + "categorie": "category", + "catégorie": "Category", + "certificatMédical": "Medical certificate", + "chargement...": "Loading...", + "chargerLexcel": "Load Excel", + "chargerLexcel.msg": "Please use the file above as a template; do not rename the columns or modify the license numbers.", + "choisir...": "Choose...", + "club.aff_renew.msg": "Please select 0 to 3 board members to fill out the pre-request. (If a non-board member will become one next year, you can enter them in the next step)", + "club.change.status": "To modify the above information, please contact the FFSAF by email.", + "club.contact.tt": { + "AUTRE": "Other club contact", + "COURRIEL": "Club email address
Example: contact@ffsaf.fr", + "FACEBOOK": "Club Facebook page starting with 'https://www.facebook.com/'
Example: https://www.facebook.com/ffmsf", + "INSTAGRAM": "Club Instagram account starting with 'https://www.instagram.com/'
Example: https://www.instagram.com/ff_msf", + "SITE": "Club website with or without 'https://'
Example: ffsaf.fr
Or https://ffsaf.fr", + "TELEPHONE": "Club phone number
Example: 06 12 13 78 55" + }, + "club.toast.aff.error": "Failed to load affiliations", + "club.toast.aff.pending": "Loading affiliations in progress", + "club.toast.aff.success": "Affiliations loaded successfully 🎉", + "club.toast.del.error": "Failed to delete club", + "club.toast.del.pending": "Deleting club in progress", + "club.toast.del.success": "Club deleted successfully 🎉", + "club.toast.new.error": "Failed to create club", + "club.toast.new.pending": "Creating club in progress", + "club.toast.new.success": "Club created successfully 🎉", + "club.toast.save.error": "Failed to save club", + "club.toast.save.pending": "Saving club in progress", + "club.toast.save.success": "Club saved successfully 🎉", + "clubExterne": "External club", + "club_one": "Club", + "club_other": "Clubs", + "club_zero": "No club", + "combattant": "fighter", + "comp.aff.blason": "Display the club's coat of arms on screens", + "comp.aff.flag": "Display the fighter's country on screens", + "comp.ajoutRapide": "Quick add", + "comp.ajouterUnCombattant": "Add a fighter", + "comp.ajouterUnInvité": "Add a guest", + "comp.billetterie": "Ticketing", + "comp.billetterieHelloasso": "HelloAsso Ticketing", + "comp.catégorieNormalisée": "Standardized category", + "comp.combattantNonTrouvé": "Fighter not found", + "comp.combattantsInscrits": "Registered fighters", + "comp.compétitionFuture": "Future competition", + "comp.compétitionPassée": "Past competition", + "comp.créationCompétition": "Competition creation", + "comp.dateDinscription": "Registration date", + "comp.editionCompétition": "Competition edition", + "comp.error1": "The end date must be after the start date.", + "comp.error2": "Please enter the start and end dates of registration.", + "comp.error3": "The end date of registration must be after the start date of registration.", + "comp.exporterLesInscription": "Export registrations", + "comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email for receiving failed registrations", + "comp.ha.error1": "Please enter the HelloAsso ticketing URL and associated rates.", + "comp.ha.error2": "The HelloAsso ticketing URL is invalid. Please check the URL format.", + "comp.ha.error3": "Please enter the email for receiving failed registrations.", + "comp.ha.tarifsHelloasso": "HelloAsso rates", + "comp.ha.text1": "To ensure good interconnection with HelloAsso, please follow these instructions:", + "comp.ha.text2": "Configure the notification URL: In order for us to receive a notification for each registration, it is necessary to configure the notification URL of your HelloAsso account to redirect to \"https://intra.ffsaf.fr/api/webhook/ha\". To do this, from the home page of your association on HelloAsso, go to My account > Settings > Integrations and API section Notification and copy-paste https://intra.ffsaf.fr/api/webhook/ha in the My callback URL field and save.", + "comp.ha.text3": "Copy-paste the exact name of the rates-separated by semicolons- that will result in automatic registration. All these rates must imperatively require the license number as a mandatory field. To do this, during the configuration of your ticketing at step 3, click on + Add information, enter the exact title License number, in Type of response desired enter Number, select the rates entered previously and make the information mandatory.", + "comp.ha.text4": "Copy-paste the URL of your ticketing in the field below. It should look like this: https://www.helloasso.com/associations/__asso-name-on-helloasso__/events/__ticketing-name__", + "comp.ha.text5": "HelloAsso ticketing URL", + "comp.ha.text6": "If for any reason the automatic registration fails, an email will be sent to this address to inform you", + "comp.informationsGénéralesSurLaCompétition": "General information about the competition", + "comp.informationsSurLeModeDinscription": "Information on the registration mode", + "comp.informationsTechniques": "Technical information", + "comp.inscription": "Registration", + "comp.inscriptionModeAdministrateur": "Registration - administrator mode", + "comp.inscriptionsLibres": "Free registrations", + "comp.inscriptionsParLesAdministrateursDeLaCompétition": "Registrations by competition administrators", + "comp.inscriptionsParLesResponsablesDeClub": "Registrations by club managers", + "comp.inscriptionsSurLaBilletterieHelloasso": "Registrations on the HelloAsso ticketing", + "comp.modal.information": "Information", + "comp.modal.poids": "Weight (in kg)", + "comp.modal.recherche": "Search*", + "comp.modal.surclassement": "Overclassification", + "comp.modal.text1": "Guests are reserved for members not licensed by the federation. Fighters registered via this form will not be able to see their results from their profile.", + "comp.modal.text2": "Prevent members/clubs from modifying this registration", + "comp.modifierLesParticipants": "View/Edit participants", + "comp.monInscription": "My registration", + "comp.noDeLicence": "License number", + "comp.nouvelleCompétition": "New competition", + "comp.organisateur": "Organizer", + "comp.quiPeutInscrire": "Who can register", + "comp.reg.libres": "Free", + "comp.reg.parLesAdministrateursDeLaCompétition": "By competition administrators", + "comp.reg.parLesResponsablesDeClub": "By club managers", + "comp.reg.surLaBilletterieHelloasso": "On the HelloAsso ticketing", + "comp.responsablesEtBureauxDesAssociations": "Managers and boards of associations", + "comp.sinscrire": "Register", + "comp.supprimerLaCompétition": "Delete competition", + "comp.supprimerLaCompétition.msg": "Are you sure you want to delete this competition and all associated results?", + "comp.surclassement_one": "{{cat}} with 1 overclassification", + "comp.surclassement_other": "{{cat}} with {{count}} overclassifications", + "comp.surclassement_zero": "{{cat}}", + "comp.text2": "Visible to the public (appears in the list of competitions)", + "comp.text3": "If not checked, the competition will only be visible to people who can register participants.", + "comp.tips": "Tip 1: It is possible to ban a fighter, which will prevent them from being re-registered by any means other than by an administrator of this competition. To do this, click on the small <1/> next to their name.
Tip 2: It is also possible to lock the modifications of their registration from their file, which will prevent it from being modified/deleted by themselves and/or a club manager.", + "comp.toast.del.error": "Failed to delete competition", + "comp.toast.del.pending": "Deleting competition in progress", + "comp.toast.del.success": "Competition deleted successfully 🎉", + "comp.toast.params.error": "Failed to update competition parameters", + "comp.toast.params.pending": "Updating competition parameters in progress...", + "comp.toast.params.success": "Competition parameters updated successfully 🎉", + "comp.toast.register.add.error": "Fighter not found", + "comp.toast.register.add.pending": "Search in progress", + "comp.toast.register.add.success": "Fighter found and added/updated", + "comp.toast.register.ban.error": "Error", + "comp.toast.register.ban.pending": "Unregistration in progress", + "comp.toast.register.ban.success": "Fighter unregistered and banned", + "comp.toast.register.del.error": "Error", + "comp.toast.register.del.pending": "Unregistration in progress", + "comp.toast.register.del.success": "Fighter unregistered", + "comp.toast.register.self.add.error": "Error during registration", + "comp.toast.register.self.add.pending": "Registration in progress", + "comp.toast.register.self.add.success": "Registration completed 🎉", + "comp.toast.register.self.del.error": "Error during unregistration", + "comp.toast.register.self.del.pending": "Unregistration in progress", + "comp.toast.register.self.del.success": "Unregistration completed", + "comp.toast.save.error": "Failed to save competition", + "comp.toast.save.pending": "Saving competition in progress", + "comp.toast.save.success": "Competition saved successfully 🎉", + "comp.tousLesMembresDeLaFfsaf": "All FFSAF members", + "comp.typeDinscription": "Registration type", + "comp.uniquementLesAdministrateursDeLaCompétition": "Only competition administrators", + "comp.warn1": "Are you sure you want to unregister and ban this fighter from the competition?\n(You can re-register them later)", + "comp.warn2": "Are you sure you want to unregister this fighter?\nThis will not unregister them from the HelloAsso ticketing and will not refund them.", + "comp.warn3": "Are you sure you want to unregister this fighter?", + "comp.warn4": "Are you sure you want to unregister yourself?", + "comp_manage": "Competitions Manager", + "competition_one": "Competition", + "competition_other": "Competitions", + "compte": "Account", + "compétition": "Competition", + "configuration": "Configuration", + "conserverLancienEmail": "Keep the old email", + "contactAdministratif": "Administrative contact", + "contactInterne": "Internal contact", + "contact_one": "Contact", + "contact_other": "Contacts", + "date": "Date", + "dateDeNaissance": "Date of birth", + "days": [ + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + "Sunday" + ], + "de": "of", + "demandeDaffiliationEnCours": "Affiliation request in progress", + "demandeDeLicence": "License request", + "demander": "Request", + "description": "Description", + "dlAff": "Download affiliation certificate", + "donnéesAdministratives": "Administrative data", + "du": "From", + "dun": "of a", + "définirLidDuCompte": "Define account ID", + "editionDeL'affiliation": "Editing affiliation", + "editionDeLaDemande": "Editing request", + "editionDeLaLicence": "Editing license", + "editionDeLaSéléction": "Editing selection", + "editionDeLadresse": "Editing address", + "email": "Email", + "en": "in", + "enAttente": "Pending", + "erreurDePaiement": "Payment error😕", + "erreurDePaiement.detail": "Error message:", + "erreurDePaiement.msg": "An error occurred while processing your payment. Please try again later.", + "espaceAdministration": "Administration space", + "f": "F", + "faitPar": "Done by", + "femme": "Female", + "filtre": "Filter", + "genre": "Gender", + "gestionGroupée": "Group management", + "gradeDarbitrage": "Refereeing grade", + "h": "M", + "home": { + "header1": "For licensed members", + "header2": "For clubs", + "text1": "Here you will find all your information as well as the status of your registration with the federation. You can also download your registration certificate, register for competitions, and consult your results, provided that the organizing club has entered them.

During your first registration, you will receive an email containing your login information; this email will be sent once your license is validated by the secretariat.", + "text2": "This is where you can take out federal licenses for your members, where you can request or renew your affiliation, enter your schedules, training locations, and social networks, which will then be displayed on the ffsaf.fr website.
You will also have the possibility to publish registration forms for your competitions and to record the results.

Not yet affiliated with the federation? Click <1>here to make your first request.", + "welcome_message": "Welcome to the intranet of the Fédération France Soft Armored Fighting" + }, + "homme": "Male", + "horairesD'entraînements": "Training schedules", + "information": "Information", + "invité": "guest", + "keepEmpty": "Leave blank to make no changes.", + "le": "the", + "licence": "License", + "licenceNo": "License no. {{no}}", + "lieu": "Place", + "lieuxDentraînements": "Training locations", + "loading": "Loading...", + "me": { + "result": { + "PRIVATE": "Private (visible only to me)", + "PUBLIC": "Public (visible to all)", + "REGISTERED_ONLY": "Logged-in members (visible to federation members)", + "REGISTERED_ONLY_NO_DETAILS": "Logged-in members - hide details (visible to federation members)" + }, + "toast.settings.error": "Failed to update settings 😕", + "toast.settings.pending": "Updating settings in progress...", + "toast.settings.success": "Settings updated successfully 🎉" + }, + "me.changerMonMotDePasse": "Change my password", + "me.formationDarbitrage": "Refereeing training", + "me.paramètresDuCompte": "Account settings", + "me.rôleAuSienDuClub": "Role within the club", + "me.visibilitéDesRésultats": "Visibility of results", + "member_one": "Member", + "member_other": "Members", + "membre.emailVideàLaLigne": "Empty email on line {{no}}", + "membre.emailVérifié": "Email verified", + "membre.filtre.inactif": "Show inactive fighters", + "membre.filtre.licence": "Show license status", + "membre.filtre.licences": [ + "No request or valid license", + "With request or valid license", + "Request in progress", + "License validated", + "All license statuses", + "Complete request", + "Incomplete request" + ], + "membre.filtre.payement": [ + "Without payment", + "With payment", + "All payment statuses" + ], + "membre.identifiant": "Identifier", + "membre.import.err1": "Invalid medical certificate date format on line {{no}}", + "membre.import.err2": "Invalid medical certificate date format on line {{no}}", + "membre.import.err3": "Empty date of birth on line {{no}}", + "membre.import.err4": "Invalid date of birth format on line {{no}}", + "membre.import.err5": "Invalid email on line {{no}}", + "membre.import.errTT_one": "{{count}} error in the file, operation cancelled", + "membre.import.errTT_other": "{{count}} errors in the file, operation cancelled", + "membre.import.warn_one": "{{count}} medical certificate not filled", + "membre.import.warn_other": "{{count}} medical certificates not filled", + "membre.info.emailInfo": "The email is used to create an account to log in to the site and must be unique.
For minors, the parents' email can be used multiple times using the following syntax: {'email.parent+@example.com'}.
Examples: mail.parent+1@example.com, mail.parent+titouan@example.com, mail.parent+cedrique@example.com", + "membre.info.error1": "Please select a valid country 😕", + "membre.info.error2": "Please select a valid club 😕", + "membre.initaccount": "Initialize account", + "membre.initaccount.text1": "Enter the account UUID", + "membre.initaccount.text2": "Warning: only change a member's ID if you are sure of what you are doing...", + "membre.noAccount": "This member does not have an account...", + "membre.noAccount.clubMsg": "An account will be created by the federation when their first license is validated", + "membre.nomVideàLaLigne": "Empty last name on line {{no}}", + "membre.prénomVideàLaLigne": "Empty first name on line {{no}}", + "membre.toast.compte.created": "Account created successfully 🎉", + "membre.toast.compte.error": "Failed to create account", + "membre.toast.compte.pending": "Creating account in progress", + "membre.toast.del.error": "Failed to delete account", + "membre.toast.del.pending": "Deleting account in progress", + "membre.toast.del.success": "Account deleted successfully 🎉", + "membre.toast.id.error": "Failed to define identifier", + "membre.toast.id.pending": "Defining identifier in progress", + "membre.toast.id.success": "Identifier defined successfully 🎉", + "membre.toast.licence.ask.del.error": "Failed to delete license request", + "membre.toast.licence.ask.del.pending": "Deleting license request in progress", + "membre.toast.licence.ask.del.success": "License request deleted successfully 🎉", + "membre.toast.licence.ask.error": "Failed to request license", + "membre.toast.licence.ask.pending": "Saving license request in progress", + "membre.toast.licence.ask.success": "License request saved successfully 🎉", + "membre.toast.licence.del.error": "Failed to delete license", + "membre.toast.licence.del.pending": "Deleting license in progress", + "membre.toast.licence.del.success": "License deleted successfully 🎉", + "membre.toast.licence.save.error": "Failed to save license", + "membre.toast.licence.save.pending": "Saving license in progress", + "membre.toast.licence.save.success": "License saved successfully 🎉", + "membre.toast.licences.export.error": "Failed to export licenses", + "membre.toast.licences.export.pending": "Exporting licenses in progress", + "membre.toast.licences.export.success": "Licenses exported successfully 🎉", + "membre.toast.licences.import.error": "Failed to send changes", + "membre.toast.licences.import.pending": "Sending changes in progress", + "membre.toast.licences.import.success": "Changes sent successfully 🎉", + "membre.toast.licences.load.error": "Failed to load licenses", + "membre.toast.licences.load.pending": "Loading licenses in progress", + "membre.toast.licences.load.success": "Licenses loaded successfully 🎉", + "membre.toast.perm.error": "Failed to update permissions 😕", + "membre.toast.perm.pending": "Updating permissions in progress...", + "membre.toast.perm.success": "Permissions updated successfully 🎉", + "membre.toast.save.error": "Failed to update profile 😕", + "membre.toast.save.pending": "Updating profile in progress...", + "membre.toast.save.success": "Profile updated successfully 🎉", + "membre.toast.select.del.error": "Failed to delete selection", + "membre.toast.select.del.pending": "Deleting selection in progress", + "membre.toast.select.del.success": "Selection deleted successfully 🎉", + "membre.toast.select.save.error": "Failed to save selection", + "membre.toast.select.save.pending": "Saving selection in progress", + "membre.toast.select.save.success": "Selection saved successfully 🎉", + "mettreàJours": "Update", + "modification": "Modification", + "nationalité": "Nationality", + "nav": { + "account": "My account", + "aff_request": "Affiliation request", + "club": { + "my": "My club" + }, + "competitions": { + "results": "My results" + }, + "home": "Home", + "login": "Login", + "logout": "Logout", + "space": "My space", + "title": "FFSAF Intranet" + }, + "noLicence": "License no.", + "noSiretOuRna": "SIRET or RNA no.", + "nom": "Last name", + "nombreDeLicences": "Number of licenses", + "nombreDeLicencesParCatégorie": "Number of licenses by category for {{saison}}", + "non": "No", + "nonDéfinie": "Not defined", + "nonValidée": "Not validated", + "nouveauClub": "New club", + "nouveauMembre": "New member", + "nouvelEmail": "New email", + "ou": "or", + "oui": "Yes", + "outdated_session": { + "login_button": "Log in again", + "message": "Your session has expired, please log in again to continue using the application.", + "title": "Session expired" + }, + "pageClub": "Club page", + "pageMembre": "Member page", + "pageNouveauClub": "New club page", + "page_info_full": "Line {{line}} to {{tt_line}} (page {{page}} of {{tt_page}})", + "page_info_ligne": "{{show}} line(s) displayed out of {{total}}", + "paiementDeLaLicence": "License payment", + "paiementDesLicences": "License payments", + "par": "by", + "pasDeLicence": "No license", + "payment.ha.info": "HelloAsso's solidarity model guarantees that 100% of your payment will be transferred to the chosen association. You can support the help they provide to associations by leaving a voluntary contribution to HelloAsso at the time of your payment.", + "payment.info": "About HelloAsso", + "payment.paiementSécurisé": "Secure payment", + "payment.payerAvec": "Pay with", + "payment.recap": "{{count}} license(s) selected
Total amount to pay: {{total}} €", + "paymentDesLicences": "License payments", + "paymentDesLicences.msg_one": "Are you sure you want to mark the license as paid?", + "paymentDesLicences.msg_other": "Are you sure you want to mark the {{count}} licenses as paid?", + "paymentDesLicences.msg_zero": "$t(paymentDesLicences.msg_other)", + "paymentOk": "🎉Your payment has been processed successfully.🎉", + "paymentOk.msg": "Thank you for your payment. The licenses should be activated within the next hour, provided that the medical certificate is completed.", + "pays": "Country", + "perm.administrateurDeLaFédération": "Federation administrator", + "perm.créerDesCompétion": "Create competitions", + "perm.ffsafIntra": "FFSAF intra", + "permission": "Permission", + "photos": "Photos", + "prenom": "First name", + "prénomEtNom": "First and last name", + "rechercher": "Search", + "rechercher...": "Search...", + "registration_one": "Registration", + "registration_other": "Registrations", + "renouveler": "Renew", + "renouvellementDeLaffiliation": "Affiliation renewal", + "result_one": "Result", + "result_other": "Results", + "retouràLaListeDeMembres": "Back to member list", + "role": "Role", + "role.membre": "Member", + "role.membreDuBureau": "Board member", + "role.président": "President", + "role.secrétaire": "Secretary", + "role.trésorier": "Treasurer", + "role.vise-président": "Vice-President", + "role.vise-secrétaire": "Vice-Secretary", + "role.vise-trésorier": "Vice-Treasurer", + "saison": "Season", + "secrétariatsDeLice": "Ring secretariats", + "selectionner...": "Select...", + "siretOuRna": "SIRET or RNA", + "stats": "Statistics", + "statue": "Statue", + "status": "Status", + "statuts": "Statutes", + "supprimerLeClub": "Delete club", + "supprimerLeClub.msg": "Are you sure you want to delete this club?", + "supprimerLeCompte": "Delete account", + "supprimerLeCompte.msg": "Are you sure you want to delete this account?", + "sélectionEnéquipeDeFrance": "Selection in the French team", + "sélectionner...": "Select...", + "toast.edit.error": "Failed to save changes", + "toast.edit.pending": "Saving changes in progress", + "toast.edit.success": "Changes saved successfully 🎉", + "toast.licence.bulk.pay.error": "Failed to mark licenses as paid", + "toast.licence.bulk.pay.pending": "Marking licenses as paid in progress", + "toast.licence.bulk.pay.success": "Licenses marked as paid successfully 🎉", + "toast.licence.bulk.valid.error": "Failed to validate licenses", + "toast.licence.bulk.valid.pending": "Validating licenses in progress", + "toast.licence.bulk.valid.success": "Licenses validated successfully 🎉", + "toast.licence.order.error": "Failed to create order", + "toast.licence.order.pending": "Creating order in progress", + "toast.licence.order.success": "Order created successfully 🎉", + "trie": "Sort", + "téléchargerLexcelDesMembres": "Download members' Excel", + "téléchargerLexcelDesMembres.info": "To be used as a template to update information", + "téléchargéeLaLicence": "Download license", + "validationDeLaLicence": "License validation", + "validationDesLicences": "License validations", + "validerDesLicences": "Validate licenses", + "validerLePayement_one": "Validate payment for the selected license", + "validerLePayement_other": "Validate payment for the {{count}} selected licenses", + "validerLePayement_zero": "$t(validerLePayement_other)", + "validerLicence.msg_one": "Are you sure you want to validate the license?", + "validerLicence.msg_other": "Are you sure you want to validate the {{count}} licenses?", + "validerLicence.msg_zero": "$t(validerLicence.msg_other)", + "validerLicence_one": "Validate the selected license", + "validerLicence_other": "Validate the {{count}} selected licenses", + "validerLicence_zero": "$t(validerLicence_other)", + "validée": "Validated", + "voir/modifierLesParticipants": "View/Edit participants", + "voirLesStatues": "View statues", + "à": "at", + "étatDeLaDemande": "Request status" +} diff --git a/src/main/webapp/public/locales/en/result.json b/src/main/webapp/public/locales/en/result.json new file mode 100644 index 0000000..094181e --- /dev/null +++ b/src/main/webapp/public/locales/en/result.json @@ -0,0 +1,67 @@ +{ + "--sélectionnerUnClub--": "--Select a club--", + "--sélectionnerUnCombattant--": "--Select a fighter--", + "--sélectionnerUneCatégorie--": "--Select a category--", + "abs.": "abs.", + "adversaire": "Opponent", + "aujourdhuià": "Today at {{time}}", + "avanthierà": "Day before yesterday at {{time}}", + "back": "« back", + "bleu": "Blue", + "catégorie": "Category", + "chargement": "Loading", + "club": "Club", + "combattant": "Fighter", + "combattants": "Fighters", + "date": "Date", + "disc.": "DQ", + "défaites": "Losses", + "erreurDeChargementDeLaListe": "Error loading the list", + "erreurDeChargementDeLaPoule": "Error loading the pool", + "erreurDeChargementDesCatégories": "Error loading categories", + "erreurDeChargementDesClubs": "Error loading clubs", + "erreurDeChargementDesCombattants": "Error loading fighters", + "erreurDeChargementDuClub": "Error loading club", + "erreurDeChargementDuCombattant": "Error loading fighter", + "for.": "forf.", + "hierà": "Yesterday at {{time}}", + "info": "Info", + "listeDesCombattants": "List of fighters", + "listeDesMatchs": "List of matches", + "listeDesMembres": "List of members", + "listeDesMenbres": "List of members", + "nom": "Last name", + "nomPrénom": "Last name First name", + "nombreDeMatchDisputé2": "Number of matches played: {{nb}}", + "nombreDeVictoires2": "Number of wins: {{nb}}", + "nombreDinscris": "Number of registered", + "nombreDinscris2": "Number of registered: {{nb}}", + "parCatégorie": "By category", + "parClub": "By club", + "parCombattant": "By fighter", + "place": "Place", + "pointsMarqués": "Points scored", + "pointsMarqués2": "Points scored: {{nb}}", + "pointsReçus": "Points received", + "pointsReçus2": "Points received: {{nb}}", + "poule": "Pool", + "ratio": "Ratio", + "ratioDePointsMoyen2": "Average points ratio: {{nb}}", + "ratioDeVictoiresMoyen2": "Average win ratio: {{nb}}", + "ratioDuScore2": "Score ratio (points scored / points received): {{nb}}", + "ratioPoints": "Points ratio", + "ratioVictoires": "Win ratio", + "ratiosPoints": "Points ratios", + "rechercheParCatégorie": "Search by category", + "rechercheParClub": "Search by club", + "rechercheParCombattant": "Search by fighter", + "rouge": "Red", + "résultatDeLaCompétition": "Competition result", + "scores": "Scores", + "statistique": "Statistics", + "tauxDeVictoire2": "Win rate: {{nb}}% ({{victoires}} out of {{matchs}})", + "tournois": "Tournaments", + "tousLesCombattants": "All fighters", + "victoire": "Win", + "victoires": "Wins" +} diff --git a/src/main/webapp/public/locales/fr/cm.json b/src/main/webapp/public/locales/fr/cm.json new file mode 100644 index 0000000..6f620c6 --- /dev/null +++ b/src/main/webapp/public/locales/fr/cm.json @@ -0,0 +1,143 @@ +{ + "--SélectionnerUnCombattant--": "-- Sélectionner un combattant --", + "--Tous--": "-- Tous --", + "actuel": "Actuel", + "administration": "Administration", + "adresseDuServeur": "Adresse du serveur", + "ajouter": "Ajouter", + "ajouterDesCombattants": "Ajouter des combattants", + "attention": "Attention", + "aucuneConfigurationObs": "Aucune configuration OBS trouvée, veuillez en importer une", + "bleu": "Bleu", + "blue": "Blue", + "catégorie": "Catégorie", + "chrono.+/-...S": "+/- ... s", + "chrono.+10S": "+10 s", + "chrono.+1S": "+1 s", + "chrono.-10S": "-10 s", + "chrono.-1S": "-1 s", + "chrono.arrêter": "Arrêter", + "chrono.définirLeTemps": "Définir le temps", + "chrono.démarrer": "Démarrer", + "chrono.editionTemps": "Edition temps", + "chrono.entrezLeTempsEnS": "Entrez le temps en s", + "chrono.recapTemps": "Temps: {{temps}}, pause: {{pause}}", + "chronomètre": "Chronomètre", + "club": "Club", + "compétition": "Compétition", + "compétitionManager": "Compétition manager", + "config.obs.dossierDesResources": "Dossier des resources", + "config.obs.motDePasseDuServeur": "Mot de passe du serveur", + "config.obs.warn1": "/! Le mot de passe va être stoker en claire, il est recommandé de ne l'utiliser que sur obs websocket et d'en changer entre chaque compétition", + "config.obs.ws": "ws://", + "configurationObs": "Configuration OBS", + "confirm1": "Ce match a déjà des résultats, êtes-vous sûr de vouloir le supprimer ?", + "confirm2.msg": "Voulez-vous vraiment changer la taille de l'arbre du tournoi ou les matchs pour les perdants ? Cela va modifier les matchs existants (incluant des possibles suppressions)!", + "confirm2.title": "Changement de l'arbre du tournoi", + "confirm3.msg": "Voulez-vous vraiment enlever la partie {{typeStr}} de la catégorie. Cela va supprimer les matchs contenus dans cette partie !", + "confirm3.title": "Changement de type de catégorie", + "confirm4.msg": "Voulez-vous vraiment supprimer la catégorie {{name}}. Cela va supprimer tous les matchs associés !", + "confirm4.title": "Suppression de la catégorie", + "conserverUniquementLesMatchsTerminés": "Conserver uniquement les matchs terminés", + "contre": "contre", + "créerLesMatchs": "Créer les matchs", + "demi-finalesEtFinales": "Demi-finales et finales", + "duréePause": "Durée pause", + "duréeRound": "Durée round", + "editionDeLaCatégorie": "Edition de la catégorie", + "enregister": "Enregister", + "enregistrer": "Enregistrer", + "epéeBouclier": "Epée bouclier", + "err1": "Un combattant ne peut pas s'affronter lui-même !", + "err2": "Le format du nom des zones de combat est invalide. Veuillez séparer les noms par des ';'.", + "err3": "Au moins un type (poule ou tournoi) doit être sélectionné.", + "erreurLorsDeLaCopieDansLePresse": "Erreur lors de la copie dans le presse-papier : ", + "erreurLorsDeLaCréationDesMatchs": "Erreur lors de la création des matchs: ", + "exporter": "Exporter", + "fermer": "Fermer", + "finalesUniquement": "Finales uniquement", + "genre": "Genre", + "genre.f": "F", + "genre.h": "H", + "genre.na": "NA", + "inscrit": "Inscrit", + "manche": "Manche", + "matchPourLesPerdantsDuTournoi": "Match pour les perdants du tournoi:", + "matches": "Matches", + "modifier": "Modifier", + "msg1": "Il y a déjà des matchs dans cette poule, que voulez-vous faire avec ?", + "neRienConserver": "Ne rien conserver", + "no": "N°", + "nom": "Nom", + "nomDesZonesDeCombat": "Nom des zones de combat <1>(séparée par des ';')", + "nouvelle...": "Nouvelle...", + "obs.préfixDesSources": "Préfix des sources", + "pays": "Pays", + "poids": "Poids", + "poule": "Poule", + "poulePour": "Poule pour: ", + "préparation...": "Préparation...", + "rouge": "Rouge", + "réinitialiser": "Réinitialiser", + "résultat": "Résultat", + "sansPoule": "Sans poule", + "sauvegarde": "Sauvegarde", + "sauvegarder": "Sauvegarder", + "score": "Score", + "score.err1": "Impossible de terminer un match nul en tournois.", + "score.spe": "Score speciaux :
-997 : disqualifié
-998 : absent
-999 : forfait", + "scores": "Scores", + "secrétariatsDeLice": "Secrétariats de lice", + "select.aucunCombattantDisponible": "Aucun combattant disponible", + "select.aucunCombattantSélectionné": "Aucun combattant sélectionné", + "select.msg1": "(0 = désactivé)", + "select.recherche": "Recherche", + "select.sélectionnerDesCombatants": "Sélectionner des combatants", + "select.à": "à", + "serveur": "Serveur", + "suivant": "Suivant", + "supprimer": "Supprimer", + "sélectionneLesModesDaffichage": "Sélectionne les modes d'affichage", + "sélectionner": "Sélectionner", + "terminé": "Terminé", + "texteCopiéDansLePresse": "Texte copié dans le presse-papier ! Collez-le dans une balise HTML sur votre WordPress.", + "toast.createCategory.error": "Erreur lors de la création de la catégorie", + "toast.createCategory.pending": "Création de la catégorie...", + "toast.createCategory.success": "Catégorie créée !", + "toast.deleteCategory.error": "Erreur lors de la suppression de la catégorie", + "toast.deleteCategory.pending": "Suppression de la catégorie...", + "toast.deleteCategory.success": "Catégorie supprimée !", + "toast.matchs.create.error": "Erreur lors de la création des matchs.", + "toast.matchs.create.pending": "Création des matchs en cours...", + "toast.matchs.create.success": "Matchs créés avec succès.", + "toast.updateCategory.error": "Erreur lors de la mise à jour de la catégorie", + "toast.updateCategory.pending": "Mise à jour de la catégorie...", + "toast.updateCategory.success": "Catégorie mise à jour !", + "toast.updateMatchScore.error": "Erreur lors de la mise à jour du score du match", + "toast.updateMatchScore.pending": "Mise à jour du score du match...", + "toast.updateMatchScore.success": "Score du match mis à jour !", + "toast.updateTrees.error": "Erreur lors de la mise à jour des arbres", + "toast.updateTrees.init.error": "Erreur lors de la création des arbres", + "toast.updateTrees.init.pending": "Création des arbres du tournoi...", + "toast.updateTrees.init.success": "Arbres créés !", + "toast.updateTrees.pending": "Mise à jour des arbres du tournoi...", + "toast.updateTrees.success": "Arbres mis à jour !", + "tournoi": "Tournoi", + "tournois": "Tournois", + "tousLesMatchs": "Tous les matchs", + "toutConserver": "Tout conserver", + "ttm.admin.obs": "Clique court : Télécharger les ressources. Clique long : Créer la configuration obs", + "ttm.admin.scripte": "Copier le scripte d'intégration", + "ttm.table.inverserLaPosition": "Inverser la position des combattants sur cette écran", + "ttm.table.obs": "Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice", + "ttm.table.pub_aff": "Ouvrir l'affichage public", + "ttm.table.pub_score": "Afficher les scores sur l'affichage public", + "type": "Type", + "téléchargement": "Téléchargement", + "téléchargementEnCours": "Téléchargement en cours...", + "téléchargementTerminé!": "Téléchargement terminé !", + "uneCatégorie": "une catégorie", + "valider": "Valider", + "zone": "Zone", + "zoneDeCombat": "Zone de combat" +} diff --git a/src/main/webapp/public/locales/fr/common.json b/src/main/webapp/public/locales/fr/common.json new file mode 100644 index 0000000..d8b246d --- /dev/null +++ b/src/main/webapp/public/locales/fr/common.json @@ -0,0 +1,543 @@ +{ + "(optionnelle)": "(optionnelle)", + "---SansClub---": "--- sans club ---", + "---ToutLesClubs---": "--- tout les clubs ---", + "---ToutLesPays---": "--- tout les pays ---", + "---TouteLesCatégories---": "--- toute les catégories ---", + "--NonLicencier--": "-- Non licencier --", + "--SélectionnerCatégorie--": "-- Sélectionner catégorie --", + "1Catégorie": "+1 catégorie", + "2Catégorie": "+2 catégorie", + "activer": "Activer", + "admin": "Administration", + "administrateur": "Administrateur", + "adresse": "Adresse", + "adresseAdministrative": "Adresse administrative", + "aff.ancienNom": "Ancien nom: {{name}}", + "aff.byMembreSim": "Par Membre similaire", + "aff.byNewMenbre": "Par Nouveau membre", + "aff.byNoLicence": "Par n° de licence", + "aff.info1": "Ce club a déjà été affilié (affiliation n°{{no}})", + "aff.membreNo": "Membre n°{{no}}", + "aff.nomDuClub": "Nom du club", + "aff.raisonDuRefus": "Raison du refus", + "aff.raisonDuRefus.msg": "Veuillez indiquer la raison du refus", + "aff.refusConfirm": "Êtes-vous sûr de vouloir refuser cette demande ?", + "aff.refuserLaDemande": "Refuser la demande", + "aff.refuserLaDemande.detail": "Êtes-vous sûr de vouloir refuser cette demande ?", + "aff.submit.error1": "Veuillez saisir un numéro de licence valide pour le membre {{id}}", + "aff.submit.error2": "Veuillez saisir un numéro de licence valide pour le membre {{id}}", + "aff.submit.error3": "Veuillez saisir un email valide pour le membre {{id}}", + "aff.toast.accept.error": "Échec de l'acceptation de l'affiliation", + "aff.toast.accept.pending": "Acceptation de l'affiliation en cours", + "aff.toast.accept.success": "Affiliation acceptée avec succès 🎉", + "aff.toast.del.error": "Échec de la suppression de la demande d'affiliation", + "aff.toast.del.pending": "Suppression de la demande d'affiliation en cours", + "aff.toast.del.success": "Demande d'affiliation supprimée avec succès 🎉", + "aff.toast.del2.error": "Échec de la suppression de l'affiliation", + "aff.toast.del2.pending": "Suppression de l'affiliation en cours", + "aff.toast.del2.success": "Affiliation supprimée avec succès 🎉", + "aff.toast.save.error": "Échec de l'enregistrement de la demande d'affiliation", + "aff.toast.save.pending": "Enregistrement de la demande d'affiliation en cours", + "aff.toast.save.success": "Demande d'affiliation enregistrée avec succès 🎉", + "aff.toast.save2.error": "Échec de l'enregistrement de l'affiliation", + "aff.toast.save2.pending": "Enregistrement de l'affiliation en cours", + "aff.toast.save2.success": "Affiliation enregistrée avec succès 🎉", + "aff_req.appuyerSurRechercher": "Appuyer sur rechercher pour compléter", + "aff_req.association": "L'association", + "aff_req.button.cancel": "Annuler ma demande", + "aff_req.button.confirm": "Confirmer ma demande d'affiliation", + "aff_req.button.save": "Enregistrer les modifications", + "aff_req.denomination": "Dénomination", + "aff_req.disposeLicence": "Dispose déjà d'une licence", + "aff_req.error1": "Le rôle du membre {{i}} est obligatoire", + "aff_req.error2": "Le format du SIRET/RNA est invalide", + "aff_req.nomDeLassociation": "Nom de l'association", + "aff_req.text1": "L'affiliation est annuelle et valable pour une saison sportive : du 1er septembre au 31 août de l’année suivante.", + "aff_req.text2": "Pour s’affilier, une association sportive doit réunir les conditions suivantes :", + "aff_req.text2.li": [ + "Avoir son siège social en France ou Principauté de Monaco", + "Être constituée conformément au chapitre 1er du titre II du livre 1er du Code du Sport", + "Poursuivre un objet social entrant dans la définition de l’article 1 des statuts de la Fédération", + "Disposer de statuts compatibles avec les principes d’organisation et de fonctionnement de la Fédération", + "Assurer en son sein la liberté d’opinion et le respect des droits de la défense, et s’interdire toute discrimination", + "Respecter les règles d’encadrement, d’hygiène et de sécurité établies par les règlements de la Fédération" + ], + "aff_req.text3": "Après validation de votre demande, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre espace FFSAF", + "aff_req.text4": "Notez que pour finaliser votre affiliation, il vous faudra :", + "aff_req.text4.li1": "Disposer d’au moins trois membres licenciés, dont le président", + "aff_req.text4.li2": "S'être acquitté des cotisations prévues par les règlements fédéraux", + "aff_req.text5": "Vous pourrez par la suite, ajouter des adresses visibles publiquement pour vos lieux d'entrainement", + "aff_req.text6": "Laissez vide pour ne rien changer. (Si un blason a déjà été envoyé lors de cette demande, il sera utilisé, sinon nous utiliserons celui de la précédant affiliation)", + "aff_req.text7": "Demande d'affiliation envoyée avec succès", + "aff_req.text8": "Une fois votre demande validée, vous recevrez un identifiant et mot de passe provisoire pour accéder à votre espace FFSAF", + "aff_req.toast.undo.error": "Échec de l'annulation de la demande d'affiliation", + "aff_req.toast.undo.pending": "Annulation de la demande d'affiliation en cours", + "aff_req.toast.undo.success": "Demande d'affiliation annulée avec succès 🎉", + "afficherLétatDesAffiliation": "Afficher l'état des affiliation", + "affiliation": "Affiliation", + "affiliationNo": "Affiliation n°{{no}}", + "ajout": "Ajout", + "ajouterUnClub": "Ajouter un club", + "ajouterUnMembre": "Ajouter un membre", + "all_season": "--- tout les saisons ---", + "au": "au", + "aucun": "Aucun", + "aucunMembreSélectionné": "Aucun membre sélectionné", + "back": "« retour", + "blason": "Blason", + "bureau": "Bureau", + "button.accepter": "Accepter", + "button.ajouter": "Ajouter", + "button.annuler": "Annuler", + "button.appliquer": "Appliquer", + "button.confirmer": "Confirmer", + "button.créer": "Créer", + "button.enregister": "Enregister", + "button.enregistrer": "Enregistrer", + "button.fermer": "Fermer", + "button.modifier": "Modifier", + "button.refuser": "Refuser", + "button.seDésinscrire": "Se désinscrire", + "button.suivant": "Suivant", + "button.supprimer": "Supprimer", + "cat.benjamin": "Benjamin", + "cat.cadet": "Cadet", + "cat.catégorieInconnue": "Catégorie inconnue", + "cat.junior": "Junior", + "cat.miniPoussin": "Mini Poussin", + "cat.minime": "Minime", + "cat.poussin": "Poussin", + "cat.senior1": "Senior 1", + "cat.senior2": "Senior 2", + "cat.superMini": "Super Mini", + "cat.vétéran1": "Vétéran 1", + "cat.vétéran2": "Vétéran 2", + "categorie": "categorie", + "catégorie": "Catégorie", + "certificatMédical": "Certificat médical", + "chargement...": "Chargement...", + "chargerLexcel": "Charger l'Excel", + "chargerLexcel.msg": "Merci d'utiliser le fichier ci-dessus comme base, ne pas renommer les colonnes ni modifier les n° de licences.", + "choisir...": "Choisir...", + "club.aff_renew.msg": "Veuillez sélectionner 0 à 3 membres du bureau pour remplir la pré-demande. (Si un membre non-bureau va le devenir l'an prochain, vous pourrez les renseigner à la prochaine étape)", + "club.change.status": "Pour modifier les informations ci-dessus, merci de contacter la FFSAF par mail.", + "club.contact.tt": { + "AUTRE": "Autre contact du club", + "COURRIEL": "Adresse e-mail du club
Exemple: contact@ffsaf.fr", + "FACEBOOK": "Page Facebook du club débutant par 'https://www.facebook.com/'
Exemple: https://www.facebook.com/ffmsf", + "INSTAGRAM": "Compte Instagram du club débutant par 'https://www.instagram.com/'
Exemple: https://www.instagram.com/ff_msf", + "SITE": "Site web du club avec ou sans le 'https://'
Exemple: ffsaf.fr
Ou https://ffsaf.fr", + "TELEPHONE": "Numéro de téléphone du club
Exemple: 06 12 13 78 55" + }, + "club.toast.aff.error": "Impossible de charger les affiliations", + "club.toast.aff.pending": "Chargement des affiliations en cours", + "club.toast.aff.success": "Affiliations chargées avec succès 🎉", + "club.toast.del.error": "Échec de la suppression du club", + "club.toast.del.pending": "Suppression du club en cours", + "club.toast.del.success": "Club supprimé avec succès 🎉", + "club.toast.new.error": "Échec de la création du club", + "club.toast.new.pending": "Création du club en cours", + "club.toast.new.success": "Club créé avec succès 🎉", + "club.toast.save.error": "Échec de l'enregistrement du club", + "club.toast.save.pending": "Enregistrement du club en cours", + "club.toast.save.success": "Club enregistré avec succès 🎉", + "clubExterne": "Club externe", + "club_one": "Club", + "club_other": "Clubs", + "club_zero": "Sans club", + "combattant": "combattant", + "comp.aff.blason": "Afficher le blason du club sur les écrans", + "comp.aff.flag": "Afficher le pays du combattant sur les écrans", + "comp.ajoutRapide": "Ajout rapide", + "comp.ajouterUnCombattant": "Ajouter un combattant", + "comp.ajouterUnInvité": "Ajouter un invité", + "comp.billetterie": "Billetterie", + "comp.billetterieHelloasso": "Billetterie HelloAsso", + "comp.catégorieNormalisée": "Catégorie normalisée", + "comp.combattantNonTrouvé": "Combattant non trouvé", + "comp.combattantsInscrits": "Combattants inscrits", + "comp.compétitionFuture": "Compétition future", + "comp.compétitionPassée": "Compétition passée", + "comp.créationCompétition": "Création compétition", + "comp.dateDinscription": "Date d'inscription", + "comp.editionCompétition": "Edition compétition", + "comp.error1": "La date de fin doit être postérieure à la date de début.", + "comp.error2": "Veuillez renseigner les dates de début et de fin d'inscription.", + "comp.error3": "La date de fin d'inscription doit être postérieure à la date de début d'inscription.", + "comp.exporterLesInscription": "Exporter les inscription", + "comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email de réception des inscriptions échoué", + "comp.ha.error1": "Veuillez renseigner l'URL de la billetterie HelloAsso et les tarifs associés.", + "comp.ha.error2": "L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.", + "comp.ha.error3": "Veuillez renseigner l'email de réception des inscriptions échouées.", + "comp.ha.tarifsHelloasso": "Tarifs HelloAsso", + "comp.ha.text1": "Afin de permettre une bonne interconnexion avec HelloAsso, merci de suivre les instructions suivantes :", + "comp.ha.text2": "Configurer l'url de notification : afin que nous puissions recevoir une notification à chaque inscription, il est nécessaire de configurer l'url de notification de votre compte HelloAsso pour qu'il redirige vers \"https://intra.ffsaf.fr/api/webhook/ha\". Pour ce faire, depuis la page d'accueil de votre association sur HelloAsso, allez dans Mon compte > Paramètres > Intégrations et API section Notification et copier-coller https://intra.ffsaf.fr/api/webhook/ha dans le champ Mon URL de callback et enregister.", + "comp.ha.text3": "Copier-coller le nom exacte des tarifs -sépare par des point-virgules- qui donneront lieux à une inscription automatique. Tous ces tarifs doivent impérativement demander le numéro de licence en champs obligatoire. Pour ce faire, lors de la configuration de votre billetterie à l'étape n°3, cliquer sur + Ajouter une information, saisissez l'intituler exact suivant Numéro de licence, dans Type de réponse souhaitée rentrer Nombre, sélectionner les tarifs entrés plus précédemment et rendre l'information obligatoire.", + "comp.ha.text4": "Copier-coller l'url de votre billetterie dans le champs si dessous. Il devrais avoir la forme suivante: https://www.helloasso.com/associations/__nom-asso-sur-helloasso__/evenements/__nom-billetterie__", + "comp.ha.text5": "Url de la billetterie HelloAsso", + "comp.ha.text6": "Si pour une raison quelconque l'inscription automatique échoue, un email sera envoyé à cette adresse pour vous en informer", + "comp.informationsGénéralesSurLaCompétition": "Informations générales sur la compétition", + "comp.informationsSurLeModeDinscription": "Informations sur le mode d'inscription", + "comp.informationsTechniques": "Informations techniques", + "comp.inscription": "Inscription", + "comp.inscriptionModeAdministrateur": "Inscription - mode administrateur", + "comp.inscriptionsLibres": "Inscriptions libres", + "comp.inscriptionsParLesAdministrateursDeLaCompétition": "Inscriptions par les administrateurs de la compétition", + "comp.inscriptionsParLesResponsablesDeClub": "Inscriptions par les responsables de club", + "comp.inscriptionsSurLaBilletterieHelloasso": "Inscriptions sur la billetterie HelloAsso", + "comp.modal.information": "Information", + "comp.modal.poids": "Poids (en kg)", + "comp.modal.recherche": "Recherche*", + "comp.modal.surclassement": "Surclassement", + "comp.modal.text1": "Les invités sont réservés aux membres non licenciés par la fédération. Les combattants inscrits via ce formulaire ne pourront pas voir leur résultat depuis leur profil.", + "comp.modal.text2": "Empêcher les membres/club de modifier cette inscription", + "comp.modifierLesParticipants": "Voir/Modifier les participants", + "comp.monInscription": "Mon inscription", + "comp.noDeLicence": "N° de licence", + "comp.nouvelleCompétition": "Nouvelle compétition", + "comp.organisateur": "Organisateur", + "comp.quiPeutInscrire": "Qui peut inscrire", + "comp.reg.libres": "Libres", + "comp.reg.parLesAdministrateursDeLaCompétition": "Par les administrateurs de la compétition", + "comp.reg.parLesResponsablesDeClub": "Par les responsables de club", + "comp.reg.surLaBilletterieHelloasso": "Sur la billetterie HelloAsso", + "comp.responsablesEtBureauxDesAssociations": "Responsables et bureaux des associations", + "comp.sinscrire": "S'inscrire", + "comp.supprimerLaCompétition": "Supprimer la compétition", + "comp.supprimerLaCompétition.msg": "Êtes-vous sûr de vouloir supprimer cette compétition est tout les resultat associer?", + "comp.surclassement_one": "{{cat}} avec 1 de surclassement", + "comp.surclassement_other": "{{cat}} avec {{count}} de surclassements", + "comp.surclassement_zero": "{{cat}}", + "comp.text2": "Visible par le public (Apparaît dans la liste des compétitions)", + "comp.text3": "Si non coché, la compétition ne sera visible que par les personnes pouvant y inscrire des participants.", + "comp.tips": "Tips 1: Il est possible de bannir un combattant, ce qui l'empêchera d'être réinscrit par un autre moyen que par un administrateur de cette compétition. Pour cela, cliquez sur la petite <1/> à côté de son nom.
Tips 2: Il est aussi possible de verrouiller les modifications de son inscription depuis sa fiche, ce qui l'empêchera d'être modifié/supprimé par lui-même et/ou un responsable de club.", + "comp.toast.del.error": "Échec de la suppression de la compétition", + "comp.toast.del.pending": "Suppression de la compétition en cours", + "comp.toast.del.success": "Compétition supprimée avec succès 🎉", + "comp.toast.params.error": "Échec de la mise à jours des paramètres de la compétition", + "comp.toast.params.pending": "Mise à jours des paramètres de la compétition en cours...", + "comp.toast.params.success": "Paramètres de la compétition mis à jours avec succès 🎉", + "comp.toast.register.add.error": "Combattant non trouvé", + "comp.toast.register.add.pending": "Recherche en cours", + "comp.toast.register.add.success": "Combattant trouvé et ajouté/mis à jour", + "comp.toast.register.ban.error": "Erreur", + "comp.toast.register.ban.pending": "Désinscription en cours", + "comp.toast.register.ban.success": "Combattant désinscrit et bannie", + "comp.toast.register.del.error": "Erreur", + "comp.toast.register.del.pending": "Désinscription en cours", + "comp.toast.register.del.success": "Combattant désinscrit", + "comp.toast.register.self.add.error": "Erreur lors de l'inscription", + "comp.toast.register.self.add.pending": "Inscription en cours", + "comp.toast.register.self.add.success": "Inscription réalisée 🎉", + "comp.toast.register.self.del.error": "Erreur lors de la désinscription", + "comp.toast.register.self.del.pending": "Désinscription en cours", + "comp.toast.register.self.del.success": "Désinscription réalisée", + "comp.toast.save.error": "Échec de l'enregistrement de la compétition", + "comp.toast.save.pending": "Enregistrement de la compétition en cours", + "comp.toast.save.success": "Compétition enregistrée avec succès 🎉", + "comp.tousLesMembresDeLaFfsaf": "Tous les membres de la FFSAF", + "comp.typeDinscription": "Type d'inscription", + "comp.uniquementLesAdministrateursDeLaCompétition": "Uniquement les administrateurs de la compétition", + "comp.warn1": "Êtes-vous sûr de vouloir désinscrire et bannir ce combattant de la compétition?\n(Vous pouvez le réinscrire plus tard)\"", + "comp.warn2": "Êtes-vous sûr de vouloir désinscrire ce combattant ?\nCela ne le désinscrira pas de la billetterie HelloAsso et ne le remboursera pas.", + "comp.warn3": "Êtes-vous sûr de vouloir désinscrire ce combattant ?", + "comp.warn4": "Êtes-vous sûr de vouloir vous désinscrire ?", + "comp_manage": "Compétitions Manager", + "competition_one": "Compétition", + "competition_other": "Compétitions", + "compte": "Compte", + "compétition": "Compétition", + "configuration": "Configuration", + "conserverLancienEmail": "Conserver l'ancien email", + "contactAdministratif": "Contact administratif", + "contactInterne": "Contact interne", + "contact_one": "Contact", + "contact_other": "Contacts", + "date": "Date", + "dateDeNaissance": "Date de naissance", + "days": [ + "Lundi", + "Mardi", + "Mercredi", + "Jeudi", + "Vendredi", + "Samedi", + "Dimanche" + ], + "de": "de", + "demandeDaffiliationEnCours": "Demande d'affiliation en cours", + "demandeDeLicence": "Demande de licence ", + "demander": "Demander", + "description": "Description", + "dlAff": "Téléchargée l'attestation d'affiliation", + "donnéesAdministratives": "Données administratives", + "du": "Du", + "dun": "d'un", + "définirLidDuCompte": "Définir l'id du compte", + "editionDeL'affiliation": "Edition de l'affiliation", + "editionDeLaDemande": "Edition de la demande ", + "editionDeLaLicence": "Edition de la licence", + "editionDeLaSéléction": "Edition de la séléction", + "editionDeLadresse": "Edition de l'adresse", + "email": "Email", + "en": "en", + "enAttente": "En attente", + "erreurDePaiement": "Erreur de paiement😕", + "erreurDePaiement.detail": "Message d'erreur :", + "erreurDePaiement.msg": "Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.", + "espaceAdministration": "Espace administration", + "f": "F", + "faitPar": "Fait par", + "femme": "Femme", + "filtre": "Filtre", + "genre": "Genre", + "gestionGroupée": "Gestion groupée", + "gradeDarbitrage": "Grade d'arbitrage", + "h": "H", + "home": { + "header1": "Pour les licenciés", + "header2": "Pour les clubs", + "text1": "Vous y retrouverez toutes vos informations ainsi que l'état de votre inscription à la fédération. Vous pouvez également télécharger votre attestation d'inscription, vous inscrire aux compétitions ainsi que consulter vos résultats sous réserve que le club organisateur les ait renseignés.

Lors de votre première inscription, vous recevrez un email contenant vos informations d'identification, ce mail sera envoyé une fois votre licence validée par le secrétariat.", + "text2": "C'est ici que vous pouvez prendre les licences fédérales pour vos adhérents, que vous pouvez demander ou renouveler votre affiliation, renseigner vos horaires, lieux d'entraînement et réseaux sociaux qui seront par la suite affichés sur le site ffsaf.fr.
Vous aurez par ailleurs la possibilité de publier des formulaires d'inscriptions pour vos compétitions ainsi que d'enregistrer les résultats.

Vous n'êtes pas encore affilié à la fédération ? Cliquez <1>içi pour faire votre première demande.", + "welcome_message": "Bienvenue sur l’intranet de la Fédération France Soft Armored Fighting" + }, + "homme": "Homme", + "horairesD'entraînements": "Horaires d'entraînements", + "information": "Information", + "invité": "invité", + "keepEmpty": "Laissez vide pour ne rien changer.", + "le": "le", + "licence": "Licence", + "licenceNo": "Licence n°{{no}}", + "lieu": "Lieu", + "lieuxDentraînements": "Lieux d'entraînements", + "loading": "Chargement...", + "me": { + "result": { + "PRIVATE": "Privé (visible uniquement par moi)", + "PUBLIC": "Public (visible par tous)", + "REGISTERED_ONLY": "Membres connectés (visibles par les membres de la fédération)", + "REGISTERED_ONLY_NO_DETAILS": "Membres connectés - masquer les détails (visibles par les membres de la fédération)" + }, + "toast.settings.error": "Échec de la mise à jours des paramètres 😕", + "toast.settings.pending": "Mise à jours des paramètres en cours...", + "toast.settings.success": "Paramètres mis à jours avec succès 🎉" + }, + "me.changerMonMotDePasse": "Changer mon mot de passe", + "me.formationDarbitrage": "Formation d'arbitrage", + "me.paramètresDuCompte": "Paramètres du compte", + "me.rôleAuSienDuClub": "Rôle au sien du club", + "me.visibilitéDesRésultats": "Visibilité des résultats", + "member_one": "Membre", + "member_other": "Membres", + "membre.emailVideàLaLigne": "Email vide à la ligne {{no}}", + "membre.emailVérifié": "Email vérifié", + "membre.filtre.inactif": "Afficher les combattants inactifs", + "membre.filtre.licence": "Afficher l'état des licences", + "membre.filtre.licences": [ + "Sans demande ni licence validée", + "Avec demande ou licence validée", + "Demande en cours", + "Licence validée", + "Tout les états de licences", + "Demande complet", + "Demande incomplet" + ], + "membre.filtre.payement": [ + "Sans paiement", + "Avec paiement", + "Tout les états de paiement" + ], + "membre.identifiant": "Identifiant", + "membre.import.err1": "Format de la date de certificat invalide à la ligne {{no}}", + "membre.import.err2": "Format de la date de certificat invalide à la ligne {{no}}", + "membre.import.err3": "Date de naissance vide à la ligne {{no}}", + "membre.import.err4": "Format de la date de naissance invalide à la ligne {{no}}", + "membre.import.err5": "Email invalide à la ligne {{no}}", + "membre.import.errTT_one": "{{count}} erreur dans le fichier, opération annulée", + "membre.import.errTT_other": "{{count}} erreurs dans le fichier, opération annulée", + "membre.import.warn_one": "{{count}} certificat médical non rempli", + "membre.import.warn_other": "{{count}} certificats médicaux non remplis", + "membre.info.emailInfo": "L'email sert à la création de compte pour se connecter au site et doit être unique.
Pour les mineurs, l'email des parents peut être utilisé plusieurs fois grâce à la syntaxe suivante : {'email.parent+@exemple.com'}.
Exemples : mail.parent+1@exemple.com, mail.parent+titouan@exemple.com, mail.parent+cedrique@exemple.com", + "membre.info.error1": "Veuillez sélectionner un pays valide 😕", + "membre.info.error2": "Veuillez sélectionner un club valide 😕", + "membre.initaccount": "Initialiser le compte", + "membre.initaccount.text1": "Entré l'UUID du compte", + "membre.initaccount.text2": "Attention ne changée l'id d'un membre que si vous êtes sûr de ce que vos faites...", + "membre.noAccount": "Ce membre ne dispose pas de compte...", + "membre.noAccount.clubMsg": "Un compte sera créé par la fédération lors de la validation de sa première licence", + "membre.nomVideàLaLigne": "Nom vide à la ligne {{no}}", + "membre.prénomVideàLaLigne": "Prénom vide à la ligne {{no}}", + "membre.toast.compte.created": "Compte créé avec succès 🎉", + "membre.toast.compte.error": "Échec de la création du compte", + "membre.toast.compte.pending": "Création du compte en cours", + "membre.toast.del.error": "Échec de la suppression du compte", + "membre.toast.del.pending": "Suppression du compte en cours", + "membre.toast.del.success": "Compte supprimé avec succès 🎉", + "membre.toast.id.error": "Échec de la définition de l'identifient", + "membre.toast.id.pending": "Définition de l'identifient en cours", + "membre.toast.id.success": "Identifient défini avec succès 🎉", + "membre.toast.licence.ask.del.error": "Échec de la suppression de la demande de licence", + "membre.toast.licence.ask.del.pending": "Suppression de la demande de licence en cours", + "membre.toast.licence.ask.del.success": "Demande de licence supprimée avec succès 🎉", + "membre.toast.licence.ask.error": "Échec de la demande de licence", + "membre.toast.licence.ask.pending": "Enregistrement de la demande de licence en cours", + "membre.toast.licence.ask.success": "Demande de licence enregistrée avec succès 🎉", + "membre.toast.licence.del.error": "Échec de la suppression de la licence", + "membre.toast.licence.del.pending": "Suppression de la licence en cours", + "membre.toast.licence.del.success": "Licence supprimée avec succès 🎉", + "membre.toast.licence.save.error": "Échec de l'enregistrement de la licence", + "membre.toast.licence.save.pending": "Enregistrement de la licence en cours", + "membre.toast.licence.save.success": "Licence enregistrée avec succès 🎉", + "membre.toast.licences.export.error": "Échec de l'export des licences", + "membre.toast.licences.export.pending": "Export des licences en cours", + "membre.toast.licences.export.success": "Licences exportées avec succès 🎉", + "membre.toast.licences.import.error": "Échec de l'envoie des changements", + "membre.toast.licences.import.pending": "Envoie des changements en cours", + "membre.toast.licences.import.success": "Changements envoyés avec succès 🎉", + "membre.toast.licences.load.error": "Impossible de charger les licences", + "membre.toast.licences.load.pending": "Chargement des licences en cours", + "membre.toast.licences.load.success": "Licences chargées avec succès 🎉", + "membre.toast.perm.error": "Échec de la mise à jours des permissions 😕", + "membre.toast.perm.pending": "Mise à jours des permissions en cours...", + "membre.toast.perm.success": "Permission mise à jours avec succès 🎉", + "membre.toast.save.error": "Échec de la mise à jours du profil 😕", + "membre.toast.save.pending": "Mise à jours du profil en cours...", + "membre.toast.save.success": "Profil mis à jours avec succès 🎉", + "membre.toast.select.del.error": "Échec de la suppression de la séléction", + "membre.toast.select.del.pending": "Suppression de la séléction en cours", + "membre.toast.select.del.success": "Séléction supprimée avec succès 🎉", + "membre.toast.select.save.error": "Échec de l'enregistrement de la séléction", + "membre.toast.select.save.pending": "Enregistrement de la séléction en cours", + "membre.toast.select.save.success": "Séléction enregistrée avec succès 🎉", + "mettreàJours": "Mettre à jours", + "modification": "Modification", + "nationalité": "Nationalité", + "nav": { + "account": "Mon compte", + "aff_request": "Demande d'affiliation", + "club": { + "my": "Mon club" + }, + "competitions": { + "results": "Mes résultats" + }, + "home": "Accueil", + "login": "Connexion", + "logout": "Déconnexion", + "space": "Mon espace", + "title": "FFSAF Intranet" + }, + "noLicence": "N° Licence", + "noSiretOuRna": "N° $t(siretOuRna)", + "nom": "Nom", + "nombreDeLicences": "Nombre de licences", + "nombreDeLicencesParCatégorie": "Nombre de licences par catégorie pour {{saison}}", + "non": "Non", + "nonDéfinie": "Non définie", + "nonValidée": "Non validée", + "nouveauClub": "Nouveau club", + "nouveauMembre": "Nouveau membre", + "nouvelEmail": "Nouvel email", + "ou": "Ou", + "oui": "Oui", + "outdated_session": { + "login_button": "Se reconnecter", + "message": "Votre session a expirée, veuillez vous reconnecter pour continuer à utiliser l'application.", + "title": "Session expirée" + }, + "pageClub": "Page club", + "pageMembre": "Page membre", + "pageNouveauClub": "Page nouveau club", + "page_info_full": "Ligne {{line}} à {{tt_line}} (page {{page}} sur {{tt_page}})", + "page_info_ligne": "{{show}} ligne(s) affichée(s) sur {{total}}", + "paiementDeLaLicence": "Paiement de la licence", + "paiementDesLicences": "Paiement des licences", + "par": "par", + "pasDeLicence": "Pas de licence", + "payment.ha.info": "Le modèle solidaire de HelloAsso garantit que 100% de votre paiement sera versé à l’association choisie. Vous pouvez soutenir l’aide qu’ils apportent aux associations en laissant une contribution volontaire à HelloAsso au moment de votre paiement.", + "payment.info": "A propos de HelloAsso", + "payment.paiementSécurisé": "Paiement sécurisé", + "payment.payerAvec": "Payer avec", + "payment.recap": "{{count}} licence(s) sélectionnée
Total à régler : {{total}} €", + "paymentDesLicences": "Payment des licences", + "paymentDesLicences.msg_one": "Êtes-vous sûr de vouloir marquer comme payées la licence ?", + "paymentDesLicences.msg_other": "Êtes-vous sûr de vouloir marquer comme payées les {{count}} licences ?", + "paymentDesLicences.msg_zero": "$t(paymentDesLicences.msg_other)", + "paymentOk": "🎉Votre paiement a été traité avec succès.🎉", + "paymentOk.msg": "Merci pour votre paiement. Les licences devraient être activées dans l'heure qui vient, à condition que le certificat médical soit rempli.", + "pays": "Pays", + "perm.administrateurDeLaFédération": "Administrateur de la fédération", + "perm.créerDesCompétion": "Créer des compétion", + "perm.ffsafIntra": "FFSAF intra", + "permission": "Permission", + "photos": "Photos", + "prenom": "Prénom", + "prénomEtNom": "Prénom et nom", + "rechercher": "Rechercher", + "rechercher...": "Rechercher...", + "registration_one": "Inscription", + "registration_other": "Inscriptions", + "renouveler": "Renouveler", + "renouvellementDeLaffiliation": "Renouvellement de l'affiliation", + "result_one": "Résultat", + "result_other": "Résultats", + "retouràLaListeDeMembres": "Retour à la liste de membres", + "role": "Rôle", + "role.membre": "Membre", + "role.membreDuBureau": "Membre du bureau", + "role.président": "Président", + "role.secrétaire": "Secrétaire", + "role.trésorier": "Trésorier", + "role.vise-président": "Vise-Président", + "role.vise-secrétaire": "Vise-Secrétaire", + "role.vise-trésorier": "Vise-Trésorier", + "saison": "Saison", + "secrétariatsDeLice": "Secrétariats de lice", + "selectionner...": "Sélectionner...", + "siretOuRna": "SIRET ou RNA", + "stats": "Statistiques", + "statue": "Statue", + "status": "Status", + "statuts": "Statuts", + "supprimerLeClub": "Supprimer le club", + "supprimerLeClub.msg": "Êtes-vous sûr de vouloir supprimer ce club ?", + "supprimerLeCompte": "Supprimer le compte", + "supprimerLeCompte.msg": "Êtes-vous sûr de vouloir supprimer ce compte ?", + "sélectionEnéquipeDeFrance": "Sélection en équipe de France", + "sélectionner...": "Sélectionner...", + "toast.edit.error": "Échec de l'enregistrement des modifications", + "toast.edit.pending": "Enregistrement des modifications en cours", + "toast.edit.success": "Modifications enregistrées avec succès 🎉", + "toast.licence.bulk.pay.error": "Échec du marquage des licences comme payées", + "toast.licence.bulk.pay.pending": "Marquage des licences comme payées en cours", + "toast.licence.bulk.pay.success": "Licences marquées comme payées avec succès 🎉", + "toast.licence.bulk.valid.error": "Échec de la validation des licences", + "toast.licence.bulk.valid.pending": "Validation des licences en cours", + "toast.licence.bulk.valid.success": "Licences validées avec succès 🎉", + "toast.licence.order.error": "Échec de le création de la commande", + "toast.licence.order.pending": "Création de la commande en cours", + "toast.licence.order.success": "Commande créée avec succès 🎉", + "trie": "Trie", + "téléchargerLexcelDesMembres": "Télécharger l'Excel des membres", + "téléchargerLexcelDesMembres.info": "À utiliser comme template pour mettre à jour les informations", + "téléchargéeLaLicence": "Téléchargée la licence", + "validationDeLaLicence": "Validation de la licence", + "validationDesLicences": "Validation des licences", + "validerDesLicences": "Valider des licences", + "validerLePayement_one": "Valider le payement de la licence sélectionnée", + "validerLePayement_other": "Valider le payement des {{count}} licences sélectionnées", + "validerLePayement_zero": "$t(validerLePayement_other)", + "validerLicence.msg_one": "Êtes-vous sûr de vouloir valider la licence ?", + "validerLicence.msg_other": "Êtes-vous sûr de vouloir valider les {{count}} licences ?", + "validerLicence.msg_zero": "$t(validerLicence.msg_other)", + "validerLicence_one": "Valider la licence sélectionnée", + "validerLicence_other": "Valider les {{count}} licences sélectionnées", + "validerLicence_zero": "$t(validerLicence_other)", + "validée": "Validée", + "voir/modifierLesParticipants": "Voir/Modifier les participants", + "voirLesStatues": "Voir les statues", + "à": "à", + "étatDeLaDemande": "État de la demande" +} diff --git a/src/main/webapp/public/locales/fr/result.json b/src/main/webapp/public/locales/fr/result.json new file mode 100644 index 0000000..2122fd0 --- /dev/null +++ b/src/main/webapp/public/locales/fr/result.json @@ -0,0 +1,67 @@ +{ + "--sélectionnerUnClub--": "--Sélectionner un club--", + "--sélectionnerUnCombattant--": "--Sélectionner un combattant--", + "--sélectionnerUneCatégorie--": "--Sélectionner une catégorie--", + "abs.": "abs.", + "adversaire": "Adversaire", + "aujourdhuià": "Aujourd'hui à {{time}}", + "avanthierà": "Avant-hier à {{time}}", + "back": "« retour", + "bleu": "Bleu", + "catégorie": "Catégorie", + "chargement": "Chargement", + "club": "Club", + "combattant": "Combattant", + "combattants": "Combattants", + "date": "Date", + "disc.": "disc.", + "défaites": "Défaites", + "erreurDeChargementDeLaListe": "Erreur de chargement de la liste", + "erreurDeChargementDeLaPoule": "Erreur de chargement de la poule", + "erreurDeChargementDesCatégories": "Erreur de chargement des catégories", + "erreurDeChargementDesClubs": "Erreur de chargement des clubs", + "erreurDeChargementDesCombattants": "Erreur de chargement des combattants", + "erreurDeChargementDuClub": "Erreur de chargement du club", + "erreurDeChargementDuCombattant": "Erreur de chargement du combattant", + "for.": "for.", + "hierà": "Hier à {{time}}", + "info": "Info", + "listeDesCombattants": "Liste des combattants", + "listeDesMatchs": "Liste des matchs", + "listeDesMembres": "Liste des membres", + "listeDesMenbres": "Liste des menbres", + "nom": "Nom", + "nomPrénom": "Nom Prénom", + "nombreDeMatchDisputé2": "Nombre de match disputé : {{nb}}", + "nombreDeVictoires2": "Nombre de victoires : {{nb}} ", + "nombreDinscris": "Nombre d'inscris", + "nombreDinscris2": "Nombre d'inscris : {{nb}}", + "parCatégorie": "Par catégorie", + "parClub": "Par club", + "parCombattant": "Par combattant", + "place": "Place", + "pointsMarqués": "Points marqués", + "pointsMarqués2": "Points marqués : {{nb}}", + "pointsReçus": "Points reçus", + "pointsReçus2": "Points reçus : {{nb}}", + "poule": "Poule", + "ratio": "Ratio", + "ratioDePointsMoyen2": "Ratio de points moyen : {{nb}}", + "ratioDeVictoiresMoyen2": "Ratio de victoires moyen : {{nb}}", + "ratioDuScore2": "Ratio du score (point marqué / point reçu): {{nb}}", + "ratioPoints": "Ratio points", + "ratioVictoires": "Ratio victoires", + "ratiosPoints": "Ratios points", + "rechercheParCatégorie": "Recherche par catégorie", + "rechercheParClub": "Recherche par club", + "rechercheParCombattant": "Recherche par combattant", + "rouge": "Rouge", + "résultatDeLaCompétition": "Résultat de la compétition", + "scores": "Scores", + "statistique": "Statistique", + "tauxDeVictoire2": "Taux de victoire : {{nb}}% ({{victoires}} sur {{matchs}})", + "tournois": "Tournois", + "tousLesCombattants": "Tous les combattants", + "victoire": "Victoire", + "victoires": "Victoires" +} diff --git a/src/main/webapp/src/App.jsx b/src/main/webapp/src/App.jsx index 6d89acc..f3e4930 100644 --- a/src/main/webapp/src/App.jsx +++ b/src/main/webapp/src/App.jsx @@ -14,8 +14,8 @@ import {ClubRoot, getClubChildren} from "./pages/club/ClubRoot.jsx"; import {DemandeAff, DemandeAffOk} from "./pages/DemandeAff.jsx"; import {MePage} from "./pages/MePage.jsx"; import {CompetitionRoot, getCompetitionChildren} from "./pages/competition/CompetitionRoot.jsx"; -import {FallingLines} from "react-loader-spinner"; import {getResultChildren, ResultRoot} from "./pages/result/ResultRoot.jsx"; +import {useTranslation} from "react-i18next"; const router = createBrowserRouter([ { @@ -79,10 +79,12 @@ const router = createBrowserRouter([ function GetCompetitionMangerLazy() { const CMLazy = lazy(() => import('./pages/competition/editor/CompetitionManagerRoot.jsx')) + const {t} = useTranslation(); + return -

Compétition manager

-

Chargement...

+

{t("comp_manage")}

+

{t("loading")}

}>
@@ -144,6 +146,7 @@ function Root() { function ReAuthMsg() { const {is_authenticated} = useAuth() const location = useLocation() + const {t} = useTranslation(); const notAuthPaths = [ /^\/$/s, @@ -161,15 +164,14 @@ function ReAuthMsg() { }}>
-
Session expirée
+
{t("outdated_session.title")}
-

Votre session a expirée, veuillez vous reconnecter pour continuer à - utiliser l'application.

+

{t("outdated_session.message")}

- - Accueil + + {t("nav.home")}
diff --git a/src/main/webapp/src/components/Club/ContactEditor.jsx b/src/main/webapp/src/components/Club/ContactEditor.jsx index 77c13df..2eda025 100644 --- a/src/main/webapp/src/components/Club/ContactEditor.jsx +++ b/src/main/webapp/src/components/Club/ContactEditor.jsx @@ -2,19 +2,12 @@ import {useEffect, useReducer, useState} from "react"; import {SimpleReducer} from "../../utils/SimpleReducer.jsx"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faAdd, faCircleQuestion, faTrashCan} from "@fortawesome/free-solid-svg-icons"; +import {useTranslation} from "react-i18next"; export function ContactEditor({data}) { const [state, dispatch] = useReducer(SimpleReducer, []) const [out_data, setOutData] = useState({}) - - const tooltipText = { - SITE: "Site web du club avec ou sans le 'https://'
Exemple: ffsaf.fr
Ou https://ffsaf.fr", - FACEBOOK: "Page Facebook du club débutant par 'https://www.facebook.com/'
Exemple: https://www.facebook.com/ffmsf", - TELEPHONE: "Numéro de téléphone du club
Exemple: 06 12 13 78 55", - INSTAGRAM: "Compte Instagram du club débutant par 'https://www.instagram.com/'
Exemple: https://www.instagram.com/ff_msf", - COURRIEL: "Adresse e-mail du club
Exemple: contact@ffsaf.fr", - AUTRE: "Autre contact du club", - } + const {t} = useTranslation(); useEffect(() => { let i = 0; @@ -38,7 +31,7 @@ export function ContactEditor({data}) { return
- Contacts + {t('contact', {count : 2})}
    {state.map((d, index) => { if (d.data === undefined || d.data.value === undefined) @@ -62,7 +55,7 @@ export function ContactEditor({data}) { dispatch({type: 'UPDATE_OR_ADD', payload: {id: d.id, data: {type: d.data.type, value: e.target.value}}}) }}/>
@@ -147,7 +150,7 @@ export function LocationEditorModal({modal, sendData}) {
- + setLocation(e.target.value)}/> @@ -174,9 +177,9 @@ export function LocationEditorModal({modal, sendData}) {
- +
diff --git a/src/main/webapp/src/components/ClubSelect.jsx b/src/main/webapp/src/components/ClubSelect.jsx index 844f781..8e460f7 100644 --- a/src/main/webapp/src/components/ClubSelect.jsx +++ b/src/main/webapp/src/components/ClubSelect.jsx @@ -1,6 +1,7 @@ import {LoadingProvider, useLoadingSwitcher} from "../hooks/useLoading.jsx"; import {useFetch} from "../hooks/useFetch.js"; import {AxiosError} from "./AxiosError.jsx"; +import {useTranslation} from "react-i18next"; export function ClubSelect({defaultValue, name, na = false, disabled = false}) { return @@ -11,6 +12,7 @@ export function ClubSelect({defaultValue, name, na = false, disabled = false}) { function ClubSelect_({defaultValue, name, na, disabled}) { const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/club/no_detail`, setLoading, 1) + const {t} = useTranslation(); return <> {data @@ -18,8 +20,8 @@ function ClubSelect_({defaultValue, name, na, disabled}) { @@ -31,11 +33,13 @@ function ClubSelect_({defaultValue, name, na, disabled}) { } function Def() { + const {t} = useTranslation(); + return
- +
; } diff --git a/src/main/webapp/src/components/ColoredCircle.jsx b/src/main/webapp/src/components/ColoredCircle.jsx index 55fab61..e709071 100644 --- a/src/main/webapp/src/components/ColoredCircle.jsx +++ b/src/main/webapp/src/components/ColoredCircle.jsx @@ -1,5 +1,6 @@ import {Fragment} from "react"; import './ColoredCircle.css' +import i18n from "i18next"; export const ColoredCircle = ({color, boolean}) => { const styles = {backgroundColor: '#F00'}; @@ -15,7 +16,7 @@ export const ColoredCircle = ({color, boolean}) => { }; -export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => { +export const ColoredText = ({boolean, text={true: i18n.t('oui'), false: i18n.t('non')}}) => { const styles = {color: '#F00'}; if (boolean !== undefined) { @@ -25,4 +26,4 @@ export const ColoredText = ({boolean, text={true: "Oui", false: "Non"}}) => { return {text[boolean]} -}; \ No newline at end of file +}; diff --git a/src/main/webapp/src/components/ConfirmDialog.jsx b/src/main/webapp/src/components/ConfirmDialog.jsx index b3719ad..702f34f 100644 --- a/src/main/webapp/src/components/ConfirmDialog.jsx +++ b/src/main/webapp/src/components/ConfirmDialog.jsx @@ -1,5 +1,7 @@ +import {useTranslation} from "react-i18next"; export function ConfirmDialog({title, message, onConfirm = () => {}, onCancel = () => {}, id = "confirm-delete"}) { + const {t} = useTranslation(); return -} \ No newline at end of file +} diff --git a/src/main/webapp/src/components/Input.jsx b/src/main/webapp/src/components/Input.jsx deleted file mode 100644 index cbadc22..0000000 --- a/src/main/webapp/src/components/Input.jsx +++ /dev/null @@ -1,11 +0,0 @@ -export function Input({placeholder, value, onChange}) { - return
- onChange(e.target.value)} - /> -
-} \ No newline at end of file diff --git a/src/main/webapp/src/components/ListEditor.jsx b/src/main/webapp/src/components/ListEditor.jsx deleted file mode 100644 index 52edec4..0000000 --- a/src/main/webapp/src/components/ListEditor.jsx +++ /dev/null @@ -1,83 +0,0 @@ -import {useEffect, useReducer, useState} from "react"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faPen, faTrashCan} from "@fortawesome/free-solid-svg-icons"; -import {AxiosError} from "./AxiosError.jsx"; - -function SimpleReducer(datas, action) { - switch (action.type) { - case 'ADD': - return [ - ...datas, - action.payload - ] - case 'REMOVE': - return datas.filter(data => data.id !== action.payload) - case 'UPDATE_OR_ADD': - const index = datas.findIndex(data => data.id === action.payload.id) - if (index === -1) { - return [ - ...datas, - action.payload - ] - } else { - datas[index] = action.payload - return [...datas] - } - default: - throw new Error() - } -} - -export function ListEditorTest() { - const [html, dispatch] = ListEditor(ListHTML) - - useEffect(() => { - dispatch({type: 'UPDATE_OR_ADD', payload: {id: 1, content: "data in"}}) - }, []); - - return html -} - -export function ListEditor(ListItem) { - const [modal, setModal] = useState({id: -1}) - const [state, dispatch] = useReducer(SimpleReducer, []) - - const sendAffiliation = (e) => { - - dispatch({type: 'UPDATE_OR_ADD', payload: e}) - } - - return [<> -
    - {state.map((d, index) => { - return
    - - - -
    - })} -
- - -, dispatch] -} - -function ListHTML({ - data -}) { - return
{data.content}
-} \ No newline at end of file diff --git a/src/main/webapp/src/components/MemberCustomFiels.jsx b/src/main/webapp/src/components/MemberCustomFiels.jsx index a922f33..deb4f24 100644 --- a/src/main/webapp/src/components/MemberCustomFiels.jsx +++ b/src/main/webapp/src/components/MemberCustomFiels.jsx @@ -1,11 +1,15 @@ import {useEffect, useState} from "react"; import {getCategoryFormBirthDate, getCatName} from "../utils/Tools.js"; import {useCountries} from "../hooks/useCountries.jsx"; +import i18n from "../config/i18n.js"; +import {useTranslation} from "react-i18next"; export function BirthDayField({inti_date, inti_category, required = true}) { const [date, setDate] = useState(inti_date) const [category, setCategory] = useState(inti_category) const [canUpdate, setCanUpdate] = useState(false) + const {t} = useTranslation(); + useEffect(() => { const b = category !== getCategoryFormBirthDate(new Date(date)) if (b !== canUpdate) @@ -18,19 +22,19 @@ export function BirthDayField({inti_date, inti_category, required = true}) { return <>
- Date de naissance + {t('dateDeNaissance')} setDate(e.target.value)}/>
- Catégorie + {t('catégorie')} {canUpdate && } + onClick={updateCat}>{t('mettreàJours')}}
@@ -52,14 +56,14 @@ export function OptionField({name, text, values, value, disabled = false}) { export function RoleList({name, text, value, disabled = false}) { return } diff --git a/src/main/webapp/src/components/Nav.jsx b/src/main/webapp/src/components/Nav.jsx index f3693f2..5c37075 100644 --- a/src/main/webapp/src/components/Nav.jsx +++ b/src/main/webapp/src/components/Nav.jsx @@ -3,14 +3,16 @@ import {NavLink} from "react-router-dom"; import {useAuth} from "../hooks/useAuth.jsx"; import {login, logout} from "../utils/auth.js"; import {isClubAdmin} from "../utils/Tools.js"; +import {useTranslation} from "react-i18next"; export function Nav() { + const {t} = useTranslation(); return
- + - - + + @@ -288,7 +292,8 @@ function MatchList({matches, cat, menuActions}) {
CatégorieClubNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatios points${i18next.t('catégorie')}${i18next.t('club')}${i18next.t('nom')}${i18next.t('victoires')}${i18next.t('défaites')}${i18next.t('ratioVictoires')}${i18next.t('pointsMarqués')}${i18next.t('pointsReçus')}${i18next.t('ratiosPoints')}
Z PN°{t('no')} RougeBlue{t('rouge')}{t('blue')}
- {activeMatch && } + {activeMatch && + } } @@ -362,7 +367,8 @@ function BuildTree({treeData, matches, menuActions}) { {currentMatch?.matchSelect && - } + } } @@ -379,6 +385,7 @@ function ScorePanel({matchId, matchs, match, menuActions}) { function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) { const {sendRequest} = useWS() const setLoading = useLoadingSwitcher() + const {t} = useTranslation("cm"); const [end, setEnd] = useState(match?.end || false) const [scoreIn, setScoreIn] = useState("") @@ -396,13 +403,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) { menuActions.current.saveScore = (scoreRed, scoreBlue) => { const maxRound = (Math.max(...match.scores.map(s => s.n_round), -1) + 1) || 0; const newScore = {n_round: maxRound, s1: scoreRed, s2: scoreBlue}; - toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), - { - pending: 'Sauvegarde du score...', - success: 'Score sauvegardé !', - error: 'Erreur lors de la sauvegarde du score' - } - ); + toast.promise(sendRequest('updateMatchScore', {matchId: matchId, ...newScore}), getToastMessage("toast.updateMatchScore")); } return () => menuActions.current.saveScore = undefined; }, [matchId]) @@ -443,8 +444,6 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) { const score = matchs.find(m => m.id === matchId).scores.find(s => s.n_round === round); - console.log("Updating score", matchId, round, comb, scoreIn_, score); - let newScore; if (score) { if (comb === 1) @@ -460,8 +459,6 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) { return } - console.log("Updating score", matchId, newScore); - setLoading(1) sendRequest('updateMatchScore', {matchId: matchId, ...newScore}) .finally(() => { @@ -485,7 +482,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) { if (end) { if (win(match?.scores) === 0 && match.categorie_ord === -42) { - toast.error("Impossible de terminer un match nul en tournois."); + toast.error(t('score.err1')); setEnd(false); return; } @@ -529,21 +526,18 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) { const o = [...tooltipTriggerList] o.map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)) - const tt = "Score speciaux :
" + - "-997 : disqualifié
" + - "-998 : absent
" + - "-999 : forfait" + const tt = t('score.spe') const maxRound = (match?.scores) ? (Math.max(...match.scores.map(s => s.n_round), -1) + 1) : 0; return
-
Scores {t('scores')}
- - - + + + @@ -568,7 +562,7 @@ function ScorePanel_({matchId, matchs, match, menuActions, onClickVoid_}) {
setEnd(e.target.checked)}/> - +
{ setRevers(!revers) @@ -44,8 +46,8 @@ export function PointPanel({menuActions}) { {revers && red}
- - + +
} diff --git a/src/main/webapp/src/pages/competition/editor/CMTable.jsx b/src/main/webapp/src/pages/competition/editor/CMTable.jsx index f0d6980..9e2dc71 100644 --- a/src/main/webapp/src/pages/competition/editor/CMTable.jsx +++ b/src/main/webapp/src/pages/competition/editor/CMTable.jsx @@ -14,12 +14,14 @@ import {PointPanel} from "./CMTPoint.jsx"; import {importOBSConfiguration, OBSProvider, useOBS} from "../../../hooks/useOBS.jsx"; import {SimpleIconsOBS} from "../../../assets/SimpleIconsOBS.ts"; import {toast} from "react-toastify"; +import {useTranslation} from "react-i18next"; export function CMTable() { const combDispatch = useCombsDispatch() const [catId, setCatId] = useState(-1); const menuActions = useRef({}); const {data} = useRequestWS("getRegister", null) + const {t} = useTranslation("cm"); useEffect(() => { if (data === null) @@ -33,14 +35,14 @@ export function CMTable() {
-
Chronomètre
+
{t('chronomètre')}
-
Score
+
{t('score')}
@@ -48,7 +50,7 @@ export function CMTable() {
-
Matches
+
{t('matches')}
@@ -74,6 +76,7 @@ function Menu({menuActions}) { const {connected, connect, disconnect} = useOBS(); const longPress = useRef({time: null, timer: null, button: null}); const obsModal = useRef(null); + const {t} = useTranslation("cm"); const externalWindow = useRef(null) const containerEl = useRef(document.createElement("div")) @@ -154,7 +157,7 @@ function Menu({menuActions}) { connect("ws://" + config.adresse + "/", config.password, config.assets_dir); }) .catch(() => { - toast.error("Aucune configuration OBS trouvée, veuillez en importer une"); + toast.error(t('aucuneConfigurationObs')); }); } } @@ -182,21 +185,21 @@ function Menu({menuActions}) {
+ data-bs-title={t('ttm.table.inverserLaPosition')}/> longPressDown("obs")} onMouseUp={() => longPressUp("obs")} data-bs-toggle="tooltip2" data-bs-placement="top" - data-bs-title="Clique court : Charger la configuration et se connecter. Clique long : Configuration de la lice"/> + data-bs-title={t('ttm.table.obs')}/>
+ data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('ttm.table.pub_aff')}/> + data-bs-toggle="tooltip2" data-bs-placement="top" data-bs-title={t('ttm.table.pub_score')}/> , document.getElementById("actionMenu"))} {externalWindow.current && createPortal(, containerEl.current)} @@ -213,15 +216,15 @@ function Menu({menuActions}) {
- Préfix des sources + {t('obs.préfixDesSources')} sub
- - + +
diff --git a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx index e54ad8e..9840be8 100644 --- a/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/CategoryAdminContent.jsx @@ -15,7 +15,8 @@ import {CSS} from '@dnd-kit/utilities'; import {toast} from "react-toastify"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {faTrash} from "@fortawesome/free-solid-svg-icons"; -import {win} from "../../../utils/Tools.js"; +import {getToastMessage, win} from "../../../utils/Tools.js"; +import {useTranslation} from "react-i18next"; const vite_url = import.meta.env.VITE_URL; @@ -169,6 +170,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) { const combDispatch = useCombsDispatch() const {dispatch} = useWS() const [modalId, setModalId] = useState(null) + const {t} = useTranslation("cm"); useEffect(() => { const sendRegister = ({data}) => { @@ -218,7 +220,7 @@ function AddComb({groups, setGroups, removeGroup, menuActions}) { return <>
MancheRougeBleu{t('manche')}{t('rouge')}{t('bleu')}
- - - + + + - - + + - + @@ -637,7 +638,7 @@ function MatchList({matches, cat, groups, reducer}) { setCombSelect(Number(e.target.value))}> - + {combsIDs.map((combId) => ( ))} diff --git a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx index 2a535e5..8c97604 100644 --- a/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx +++ b/src/main/webapp/src/pages/competition/editor/CompetitionManagerRoot.jsx @@ -9,12 +9,14 @@ import {CMTable} from "./CMTable.jsx"; import {ThreeDots} from "react-loader-spinner"; import {AxiosError} from "../../../components/AxiosError.jsx"; import {useFetch} from "../../../hooks/useFetch.js"; +import {useTranslation} from "react-i18next"; const vite_url = import.meta.env.VITE_URL; export default function CompetitionManagerRoot() { + const {t} = useTranslation("cm"); return <> -

Compétition manager

+

{t('compétitionManager')}

}/> @@ -40,9 +42,10 @@ function Home() { } function MakeCentralPanel({data, navigate}) { + const {t} = useTranslation("cm"); return <>
-

Compétition:

+

{t('compétition')}:

{data.sort((a, b) => new Date(b.date.split('T')[0]) - new Date(a.date.split('T')[0])).map((o) => (
  • { const timer = setInterval(() => { @@ -91,7 +95,7 @@ function WSStatus({setPerm}) { return

    {welcomeData.name}

    -
    Serveur: {t('serveur')}:
    @@ -100,16 +104,17 @@ function WSStatus({setPerm}) { function Home2({perm}) { const nav = useNavigate(); + const {t} = useTranslation("cm"); return
    -

    Sélectionne les modes d'affichage

    +

    {t('sélectionneLesModesDaffichage')}

    {perm === "ADMIN" && <> - - + + } {perm === "TABLE" && <> - + }
    diff --git a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx index 3b8cd31..a798cd6 100644 --- a/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx +++ b/src/main/webapp/src/pages/competition/editor/PubAffWindow.jsx @@ -3,6 +3,7 @@ import {usePubAffState} from "../../../hooks/useExternalWindow.jsx"; import {SmartLogoBackgroundMemo} from "../../../components/SmartLogoBackground.jsx"; import {useMemo, useRef} from 'react'; import {useWS} from "../../../hooks/useWS.jsx"; +import {useTranslation} from "react-i18next"; const vite_url = import.meta.env.VITE_URL; @@ -17,6 +18,7 @@ export function PubAffWindow({document}) { const chronoText = useRef(null) const state2 = useRef({lastColor: "#ffffff", lastTimeStr: "--:--"}) const state = usePubAffState(); + const {t} = useTranslation("cm"); document.title = "Affichage Public"; document.body.className = "bg-dark text-white overflow-hidden"; @@ -55,24 +57,24 @@ export function PubAffWindow({document}) {
    - Actuel + {t('actuel')}
    - contre + {t('contre')}
    - Suivant + {t('suivant')}
    - contre + {t('contre')}
    diff --git a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx index 0240fe3..a247106 100644 --- a/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx +++ b/src/main/webapp/src/pages/competition/editor/SelectCombModalContent.jsx @@ -3,6 +3,7 @@ import {useEffect, useReducer, useState} from "react"; import {CatList, getCatName} from "../../../utils/Tools.js"; import {CombName} from "../../../hooks/useComb.jsx"; import {useWS} from "../../../hooks/useWS.jsx"; +import {useTranslation} from "react-i18next"; function SelectReducer(state, action) { switch (action.type) { @@ -54,6 +55,7 @@ function SelectReducer(state, action) { export function SelectCombModalContent({data, setGroups}) { const country = useCountries('fr') + const {t} = useTranslation("cm"); const {dispatch} = useWS() const [dispo, dispoReducer] = useReducer(SelectReducer, {}) const [select, selectReducer] = useReducer(SelectReducer, {}) @@ -153,20 +155,20 @@ export function SelectCombModalContent({data, setGroups}) { return <>
    -

    Sélectionner des combatants

    +

    {t('select.sélectionnerDesCombatants')}

    - + setSearch(e.target.value)}/>
    - + setClub(e.target.value)}> - + {clubList.sort((a, b) => a.localeCompare(b)).map((club) => ( ))} @@ -189,50 +191,50 @@ export function SelectCombModalContent({data, setGroups}) {
    - +
    setGender((prev) => { return {...prev, H: e.target.checked} })}/> - +
    setGender((prev) => { return {...prev, F: e.target.checked} })}/> - +
    setGender((prev) => { return {...prev, NA: e.target.checked} })}/> - +
    - +
    - +
    setWeightMin(Number(e.target.value))}/>
    -
    à
    +
    {t('select.à')}
    setWeightMax(Number(e.target.value))}/>
    -
    (0 = désactivé)
    +
    {t('select.msg1')}
    @@ -240,9 +242,9 @@ export function SelectCombModalContent({data, setGroups}) {
    -
    Inscrit
    +
    {t('inscrit')}
    - {dispoFiltered && Object.keys(dispoFiltered).length === 0 &&
    Aucun combattant disponible
    } + {dispoFiltered && Object.keys(dispoFiltered).length === 0 &&
    {t('select.aucunCombattantDisponible')}
    } {Object.keys(dispoFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => (
    -
    Sélectionner
    +
    {t('sélectionner')}
    - {selectFiltered && Object.keys(selectFiltered).length === 0 &&
    Aucun combattant sélectionné
    } + {selectFiltered && Object.keys(selectFiltered).length === 0 &&
    {t('select.aucunCombattantSélectionné')}
    } {Object.keys(selectFiltered).sort((a, b) => nameCompare(data, a, b)).map((id) => (
    - +
    - + { if (/^[a-zA-Z0-9]$/.test(e.target.value)) setTargetGroupe(e.target.value) }}/> - +
    } diff --git a/src/main/webapp/src/pages/result/ResultList.jsx b/src/main/webapp/src/pages/result/ResultList.jsx index 6baccb6..ef26cfe 100644 --- a/src/main/webapp/src/pages/result/ResultList.jsx +++ b/src/main/webapp/src/pages/result/ResultList.jsx @@ -3,7 +3,7 @@ import {useLoadingSwitcher} from "../../hooks/useLoading.jsx"; import {useFetch} from "../../hooks/useFetch.js"; import {AxiosError} from "../../components/AxiosError.jsx"; import {ThreeDots} from "react-loader-spinner"; -import {useAuth} from "../../hooks/useAuth.jsx"; +import {useTranslation} from "react-i18next"; export function ResultList() { const navigate = useNavigate(); @@ -25,14 +25,15 @@ export function ResultList() { } function MakeCentralPanel({data, navigate}) { + const {t} = useTranslation(); return <>
    -

    Compétition:

    +

    {t('competition', {count: data.length})}:

    {data.sort((a, b) => new Date(b[2].split('T')[0]) - new Date(a[2].split('T')[0])).map((o) => (
  • navigate(`${o[0]}`)}>{o[1]}
  • ))} + onClick={() => navigate(`${o[0]}`)}>{o[1]}))}
    diff --git a/src/main/webapp/src/pages/result/ResultRoot.jsx b/src/main/webapp/src/pages/result/ResultRoot.jsx index e95ff48..0bba34b 100644 --- a/src/main/webapp/src/pages/result/ResultRoot.jsx +++ b/src/main/webapp/src/pages/result/ResultRoot.jsx @@ -2,10 +2,13 @@ import {LoadingProvider} from "../../hooks/useLoading.jsx"; import {Outlet} from "react-router-dom"; import {ResultList} from "./ResultList.jsx"; import {ResultView} from "./ResultView.jsx"; +import {useTranslation} from "react-i18next"; export function ResultRoot() { + const {t} = useTranslation(); + return <> -

    Résultat

    +

    {t("result", {count: 1})}

    diff --git a/src/main/webapp/src/pages/result/ResultView.jsx b/src/main/webapp/src/pages/result/ResultView.jsx index c17c07e..a314c2f 100644 --- a/src/main/webapp/src/pages/result/ResultView.jsx +++ b/src/main/webapp/src/pages/result/ResultView.jsx @@ -7,6 +7,7 @@ import React, {useEffect, useState} from "react"; import {DrawGraph} from "./DrawGraph.jsx"; import {TreeNode} from "../../utils/TreeUtils.js"; import {scoreToString} from "../../utils/CompetitionTools.js"; +import {useTranslation} from "react-i18next"; function CupImg() { return @@ -40,22 +42,23 @@ export function ResultView() { // || resultShow && resultShow === "club_all" && function MenuBar({resultShow, setResultShow}) { + const {t} = useTranslation('result'); return @@ -69,15 +72,16 @@ function MenuBar({resultShow, setResultShow}) { } function BuildMatchArray({matchs}) { + const {t} = useTranslation('result'); return <>
    N°PouleZone{t('no')}{t('poule')}{t('zone')} RougeBlue{t('rouge')}{t('blue')} Résultat{t('résultat')}
    - + - + - + @@ -94,16 +98,17 @@ function BuildMatchArray({matchs}) { } function BuildRankArray({rankArray}) { + const {t} = useTranslation('result'); return <>
    Rouge{t('rouge')} Scores{t('scores')} Bleu{t('bleu')}
    - - - - - - + + + + + + @@ -147,6 +152,7 @@ function CategoryList({uuid}) { const [catId, setCatId] = useState(null) const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/result/${uuid}/category/list`, setLoading, 1) + const {t} = useTranslation('result'); useEffect(() => { if (data && Object.keys(data).length > 0) @@ -155,7 +161,7 @@ function CategoryList({uuid}) { return <> {data ?
    -
    Catégorie
    +
    {t('catégorie')}
    setClubId(e.target.value)}> {Object.keys(data).sort((a, b) => a.toLocaleLowerCase().localeCompare(b.toLocaleLowerCase())) .map(key => )} @@ -246,6 +254,7 @@ function ClubList({uuid}) { function ClubResult({uuid, clubId}) { const setLoading = useLoadingSwitcher() const {data, refresh, error} = useFetch(`/result/${uuid}/club/${clubId}`, setLoading, 1) + const {t} = useTranslation('result'); useEffect(() => { refresh(`/result/${uuid}/club/${clubId}`) @@ -253,34 +262,34 @@ function ClubResult({uuid, clubId}) { return <> {data ? <> -

    Info :

    +

    {t('info')} :

      -
    • Nom : {data.name}
    • -
    • Nombre d'inscris : {data.nb_insc}
    • +
    • {t('nom')} : {data.name}
    • +
    • {t('nombreDinscris')} : {data.nb_insc}
    -

    Statistique :

    +

    {t('statistique')} :

      -
    • Nombre de match disputé : {data.nb_match}
    • -
    • Nombre de victoires : {data.match_w}
    • -
    • Ratio de victoires moyen : {data.ratioVictoire.toFixed(3)}
    • -
    • Points marqués : {data.pointMake}
    • -
    • Points reçus : {data.pointTake}
    • -
    • Ratio de points moyen : {data.ratioPoint.toFixed(3)}
    • +
    • {t('nombreDeMatchDisputé2', {nb: data.nb_match})}
    • +
    • {t('nombreDeVictoires2', {nb: data.match_w})}
    • +
    • {t('ratioDeVictoiresMoyen2', {nb: data.ratioVictoire.toFixed(3)})}
    • +
    • {t('pointsMarqués2', {nb: data.pointMake})}
    • +
    • {t('pointsReçus2', {nb: data.pointTake})}
    • +
    • {t('ratioDePointsMoyen2', {nb: data.ratioPoint.toFixed(3)})}
    -

    Liste des membres :

    +

    {t('listeDesMembres')} :

    PlaceNomVictoireRatioPoints marquésPoints reçus{t('place')}{t('nom')}{t('victoire')}{t('ratio')}{t('pointsMarqués')}{t('pointsReçus')}
    - - - - - - - - + + + + + + + + @@ -309,6 +318,7 @@ function CombList({uuid}) { const [combId, setCombId] = useState(null) const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/result/${uuid}/comb/list`, setLoading, 1) + const {t} = useTranslation('result'); useEffect(() => { if (data && Object.keys(data).length > 0) @@ -317,7 +327,7 @@ function CombList({uuid}) { return <> {data ?
    -
    Combattant
    +
    {t('combattant')}
    CatégorieNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatio points{t('catégorie')}{t('nom')}{t('victoires')}{t('défaites')}{t('ratioVictoires')}{t('pointsMarqués')}{t('pointsReçus')}{t('ratioPoints')}
    - - - - + + + + @@ -391,30 +405,31 @@ function CombResult({uuid, combId}) { function CombsResult({uuid}) { const setLoading = useLoadingSwitcher() const {data, error} = useFetch(`/result/${uuid}/comb`, setLoading, 1) + const {t} = useTranslation('result'); return <> {data ? <> -

    Statistique :

    +

    {t('statistique')} :

      -
    • Nombre d'inscris : {data.nb_insc}
    • -
    • Nombre de match disputé : {data.tt_match}
    • -
    • Points marqués : {data.point}
    • +
    • {t('nombreDinscris2', {nb: data.nb_insc})}
    • +
    • {t('nombreDeMatchDisputé2', {nb: data.tt_match})}
    • +
    • {t('pointsMarqués2', {nb: data.point})}
    -

    Liste des combattants :

    +

    {t('listeDesCombattants')} :

    CatégorieAdversaireScoresRatio{t('catégorie')}{t('adversaire')}{t("scores")}{t('ratio')}
    - - - - - - - - - + + + + + + + + + diff --git a/src/main/webapp/src/utils/Tools.js b/src/main/webapp/src/utils/Tools.js index a74a8e3..a91cd3a 100644 --- a/src/main/webapp/src/utils/Tools.js +++ b/src/main/webapp/src/utils/Tools.js @@ -1,4 +1,5 @@ import axios from "axios"; +import i18n from "../config/i18n.js"; const vite_url = import.meta.env.VITE_URL; @@ -78,34 +79,46 @@ export function getSaison(currentDate = new Date()) { export function getCatName(cat) { switch (cat) { case "SUPER_MINI": - return "Super Mini"; + return i18n.t('cat.superMini'); case "MINI_POUSSIN": - return "Mini Poussin"; + return i18n.t('cat.miniPoussin'); case "POUSSIN": - return "Poussin"; + return i18n.t('cat.poussin'); case "BENJAMIN": - return "Benjamin"; + return i18n.t('cat.benjamin'); case "MINIME": - return "Minime"; + return i18n.t('cat.minime'); case "CADET": - return "Cadet"; + return i18n.t('cat.cadet'); case "JUNIOR": - return "Junior"; + return i18n.t('cat.junior'); case "SENIOR1": - return "Senior 1"; + return i18n.t('cat.senior1'); case "SENIOR2": - return "Senior 2"; + return i18n.t('cat.senior2'); case "VETERAN1": - return "Vétéran 1"; + return i18n.t('cat.vétéran1'); case "VETERAN2": - return "Vétéran 2"; + return i18n.t('cat.vétéran2'); case null: - return "Catégorie inconnue"; + return i18n.t('cat.catégorieInconnue'); default: return cat; } } +export function getToastMessage(msgKey) { + return { + pending: i18n.t(msgKey + '.pending'), + success: i18n.t(msgKey + '.success'), + error: { + render({data}) { + return errFormater(data, i18n.t(msgKey + '.error')) + } + } + } +} + export function win(scores) { let sum = 0 for (const score of scores) {
    CatégorieClubNomVictoiresDéfaitesRatio victoiresPoints marquésPoints reçusRatios points{t('catégorie')}{t('club')}{t('nom')}{t('victoires')}{t('défaites')}{t('ratioVictoires')}{t('pointsMarqués')}{t('pointsReçus')}{t('ratiosPoints')}