Skip to content

Commit

Permalink
i18n done
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexRoehm committed Aug 18, 2023
1 parent c80dceb commit 8571d21
Show file tree
Hide file tree
Showing 16 changed files with 308 additions and 107 deletions.
2 changes: 0 additions & 2 deletions backend/Controllers/MessageController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ public async Task<IActionResult> Send( MessageRequest request )
var oneTimeLink = await _oneTimeLinkService.Check(request.Slug);
if (oneTimeLink!=null && oneTimeLink is OneTimeLink) {
var template = await _templateService.GetByPurpose(oneTimeLink.Purpose);
Console.WriteLine(template);

if (template != null)
success = await _emailService.Send(
_templateService.To(template),
Expand Down
2 changes: 1 addition & 1 deletion backend/Controllers/ValidationController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public ValidationController( IEmailService emailer, IOneTimeLinkService oneTimeL
public async Task<IActionResult> Validate( ValidationRequest userInput )
{
var link = await _oneTimeLinkService.Get(userInput.Purpose, userInput.Email);
var url = _oneTimeLinkService.GetFullUrl(link.Slug);
var url = _oneTimeLinkService.GetFullUrl(link.Slug, userInput.Language);
var message = await _templateService.GetMessageByPurpose(userInput.Purpose,url,userInput.Email);
_emailer.Send(userInput.Email,"Link to Calvary Chapel Freiburg Contact Form",message);
return Ok();
Expand Down
1 change: 1 addition & 0 deletions backend/Models/ValidationRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public class ValidationRequest

[Required]
public string Purpose { get; set; }
public string Language { get; set; }
}
8 changes: 4 additions & 4 deletions backend/Services/OneTimeLinkService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface IOneTimeLinkService
Task<OneTimeLink> Check( string slug );
void Invalidate( string slug );
void InvalidateOld( );
string GetFullUrl( string slug );
string GetFullUrl( string slug, string? language );

}

Expand All @@ -42,7 +42,7 @@ public OneTimeLinkService(AppSettings appSettings, ContRevDb db)
_db = db;
}

public async Task<OneTimeLink> Get( string purpose, string email ) {
public async Task<OneTimeLink> Get( string purpose, string email) {

var entry = new OneTimeLink() {
Slug = Protect(purpose+email),
Expand All @@ -55,8 +55,8 @@ public async Task<OneTimeLink> Get( string purpose, string email ) {
return entry;
}

public string GetFullUrl( string slug ) {
return _baseUrl+"message/"+slug;
public string GetFullUrl( string slug, string? language ) {
return _baseUrl+(language!=null && language.Length==2?language+"/":"")+"message/"+slug;
}

public async Task<OneTimeLink> Check( string slug ) {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"pinia": "^2.1.6",
"url": "^0.11.1",
"vue": "^3.3.4",
"vue-i18n": "9",
"vue-router": "^4.2.4"
},
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { RouterView } from 'vue-router';
</script>

<template>
Expand Down
103 changes: 97 additions & 6 deletions src/components/Header.vue
Original file line number Diff line number Diff line change
@@ -1,13 +1,49 @@
<template>
<div>
<div class="flex items-center w-full h-20 shadow-md place-content-center">
<img class="h-12" :src="logo" />
<div class="flex items-center w-full h-20 shadow-md place-content-center">

<div class="w-10/12
md:w-9/12
xl:w-8/12
2xl:w-[1536px]
flex justify-between">
<div class="relative flex flex-row items-center w-1/6 flex-nowrap">
<button id="language_id" @click.stop="showDropdown=true" class="inline-flex items-center text-sm font-medium text-center rounded-lg"
type="button">
<svg class="w-3 h-3 sm:w-4 sm:h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9">
</path>
</svg>
<div class="invisible w-0 text-xs sm:w-14 sm:visible">{{ t(locale) }}</div>
<svg class="invisible w-4 h-4 sm:visible" aria-hidden="true" fill="none" stroke="currentColor"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<div v-show="showDropdown" class="absolute bg-white border-2 rounded shadow top-10 left-6 w-44">
<ul class="py-1 text-sm text-gray-700">
<li v-for="navLocale in availableLocales as string[]" :key="navLocale" class="p-1 hover:cursor-pointer" @click="showDropdown=false">
<RouterLink :to="getRouterLink(navLocale)">{{ t(navLocale) }}</RouterLink>
</li>
</ul>
</div>
</div>
<div class="content-center flex-grow text-center align-middle">
<a href="https://ccfreiburg.de" class="inline-block">
<img :src="logo" class="m-1 h-7 sm:h-14"/>
</a>
</div>
<div class="relative flex flex-row items-center w-1/6 flex-nowrap">&nbsp;
</div>
</div>
</div>
<section class="flex flex-col place-items-center">
<div class="w-11/12 overflow-hidden -inset-y-8 sm:h-56 md:h-64 lg:h-72">
<div v-if="!noimage" class="w-11/12 overflow-hidden -inset-y-8 sm:h-56 md:h-64 lg:h-72">
<img class="object-cover w-full" :src="hero" alt="" />
</div>
<CleanContainer>
<CleanContainer v-if="$props.title">
<div class="flex flex-col items-center justify-center md:mt-4">
<div class="leading-tight sm:px-2 menu-underline">
<h1 class="font-bold tracking-widest uppercase sm:text-xl md:text-2xl text-md">
Expand All @@ -25,20 +61,75 @@
</template>

<script lang="ts">
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import CleanContainer from '../components/CleanContainer.vue';
import { useRoute } from 'vue-router';
export default {
props: {
title: String,
intro: String,
noimage: Boolean,
},
components: {CleanContainer},
name: "Header",
setup() {
const showDropdown = ref(false)
const { t, availableLocales, locale } = useI18n();
const route = useRoute()
function hide() {
showDropdown.value = false;
//ctx.emit('closeAll')
}
function closeIfClickedOutside(event: any) {
if (
event.target.name === 'language_id' ||
event.target.nodeName === 'OPTION' ||
event.target.nodeName === 'LI'
)
return;
hide();
}
function getRouterLink( local : string ) :string {
const r = route.path.substring(3,4)
if (r=="/")
return (local=="de"?"":"/"+local)+route.path.substring(3)
return (local=="de"?"":"/"+local)+route.path
}
function setLocaleFromPath() {
const routstarts = availableLocales.filter((i)=>i!="de").map((i)=>"/"+i+"/")
if (routstarts.some(substr => route.path.startsWith(substr))) {
const loc = route.path.substring(1,3)
locale.value = loc
} else locale.value = "de"
}
watch(()=>route.path, setLocaleFromPath )
function closeIfScrolled(event: any) {
closeIfClickedOutside(event)
}
onMounted(() => {
document.addEventListener('click', closeIfClickedOutside);
document.addEventListener('scroll', closeIfScrolled);
setLocaleFromPath()
})
onBeforeUnmount(() => {
document.removeEventListener('click', closeIfClickedOutside);
document.removeEventListener('scroll', closeIfScrolled);
})
return {
logo: new URL('../assets/images/logo.png', import.meta.url).href,
hero: new URL('../assets/images/Calvary Chapel 22 Winter UhlArt Fotogrfie 2022-61-Header.jpg', import.meta.url).href
hero: new URL('../assets/images/Calvary Chapel 22 Winter UhlArt Fotogrfie 2022-61-Header.jpg', import.meta.url).href,
showDropdown,
t,
availableLocales,
locale,
getRouterLink
}
}
}
Expand Down
40 changes: 40 additions & 0 deletions src/locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"de": "Deutsch",
"en": "Englisch",
"username": "Benutzername",
"password": "Passwort",
"passerror": "Benutzer oder Passwort fehlerhaft.",
"logout": "Abmelden",
"thankyou": "Danke!",
"OK": "Ok",
"cancel": "Abbruch",
"delete": "Löschen",
"error": "Fehler",
"emailform": {
"title": "Email senden",
"intro": "Hier kannst du deine Email an die ausgewählte Person oder Funktion senden",
"invalid": "Der Link ist fehlerhaft oder inzwischen veraltet. Bitte lass dir einen Neuen schicken.",
"your_email_to": "Deine Email an",
"subject": "Betreff",
"message": "Nachricht",
"done": "Deine Email wurde versandt!"
},
"contactform": {
"title": "Email senden",
"intro": "Wir haben keine Datenschutzrelevanten Funktionen auf unserer Webseite. Nur hier benötigen wir zuerst deine Emailadresse. Wir freuen uns von die zu hören!",
"who_to_contact": "Mit wem möchtest du in Kontakt treten?",
"your_email": "Deine Emailadresse",
"check_your_email": "Wir haben eine Email mit dem Link zum Kontaktformular an dich gesendet. Bitte schau in dein Postfach. Eventuell solltest du auch deinen Spam checken."
},
"templates": {
"active": "Aktiv",
"purpose": "Ziel",
"purpose_full": "Ziel der Email (Beschreibung des Empfängers)",
"to": "An",
"to_full": "Email des Empfämgers",
"from_full": "Email Konto von dem gesendet wird",
"subject": "Betreff",
"text": "Email mit Validierungslink",
"add": "Hinzufügen"
}
}
40 changes: 40 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"de": "German",
"en": "English",
"username": "Username",
"password": "Password",
"passerror": "Username or password incorrect.",
"logout": "Logout",
"thankyou": "Thank you!",
"OK": "Ok",
"cancel": "Cancel",
"delete": "Delete",
"error": "Error",
"emailform": {
"title": "Send Email",
"intro": "Here you can send your email to the requested person or team or function",
"invalid": "The Link is wrong or outdated. Please get a new link.",
"your_email_to": "Your Email to",
"subject": "Subject",
"message": "Your message",
"done": "We have sent your message!"
},
"contactform": {
"title": "Send Email",
"intro": "Our web site is free from any privacy related functionality. No cookies nor external embedded content nor tracking. For the contact form we expect you to provide your email address first. We are looking forward to hear from you!",
"who_to_contact": "Who do you want to contact?",
"your_email": "Your Email Address",
"check_your_email": "We have sent an email containing the link to the contact form to you. Please check your Inbox and maybe your Spam folder."
},
"templates": {
"active": "Active",
"purpose": "Purpose",
"purpose_full": "Purpose of Email (describe the receiver)",
"to": "To",
"to_full": "Email of the receiver",
"from_full": "Email Accont to send from",
"subject": "Subject",
"text": "Text for validation email",
"add": "Add"
}
}
28 changes: 23 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,32 @@
import './style.css'
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createI18n } from 'vue-i18n'

import App from './App.vue';
import { router } from './router';
import App from "./App.vue";
import { router } from "./router";
import en from './locales/en.json';
import de from './locales/de.json';

type MessageSchema = typeof en
const config = {
legacy: false,
locale: "de",
fallbackLocale: "de",
availableLocales: ["de","en"],
messages: {
de,
en
},
}

const i18n = createI18n<[MessageSchema], 'en' | 'de'>(config);
const app = createApp(App);

app.use(createPinia());
app.use(router);
app.use(i18n);

app.mount("#app");

app.mount('#app');
export default config.availableLocales
32 changes: 25 additions & 7 deletions src/router.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { RouteLocation, createRouter, createWebHistory } from 'vue-router';

import availableLocales from './main';
import { useAuthStore } from './stores';
import Login from './views/Login.vue';
import Contact from './views/Contact.vue';
Expand All @@ -11,25 +11,43 @@ export const router = createRouter({
linkActiveClass: 'active',
routes: [
{ path: '/', component: Contact },
{ path: '/en', component: Contact },
{ path: '/templates', component: Templates },
{ path: '/en/templates', component: Templates },
{ path: '/message/:slug', component: EmailForm },
{ path: '/login', component: Login }
{ path: '/en/message/:slug', component: EmailForm },
{ path: '/login', component: Login },
{ path: '/en/login', component: Login }
]
});

router.beforeEach(async (to: RouteLocation) => {
// redirect to login page if not logged in and trying to access a restricted page

var to_lang_part = to.path.substring(1,3);
var to_compare_part = to.path.substring(3);
if (availableLocales && availableLocales.includes(to_lang_part)) {
to_lang_part = "/" + to_lang_part
} else {
to_lang_part = ""
to_compare_part = to.path
}
if (to_compare_part=="") to_compare_part ="/"
console.log(to_lang_part)
console.log(to_compare_part)

// set the current language for vuex-i18n. note that translation data
// for the language might need to be loaded first
const publicPages = ['/login', '/'];
// http://localhost:5084/message/24A8B9942DCE9A125A7E3A7918BDED84D86A7CFDBD89A88EAA0157E1B8CDA43C
const isOneTimeToken = to.path.startsWith('/message') && to.path.length==73 // length of message + SHA256
const authRequired = !publicPages.includes(to.path) && !isOneTimeToken;
const isOneTimeToken = to_compare_part.startsWith('/message') && to_compare_part.length==73 // length of message + SHA256
const authRequired = !publicPages.includes(to_compare_part) && !isOneTimeToken;
// redirect to login page if not logged in and trying to access a restricted page

const auth = useAuthStore();
if (authRequired && !auth.user) {
auth.returnUrl = to.fullPath;
return '/login';
return to_lang_part+'/login';
}

});


Loading

0 comments on commit 8571d21

Please sign in to comment.