diff --git a/frontend/src/services/i18n/i18n.js b/frontend/src/services/i18n/i18n.js index 7061f6ba6..fd383b1b6 100644 --- a/frontend/src/services/i18n/i18n.js +++ b/frontend/src/services/i18n/i18n.js @@ -1,3 +1,5 @@ +import moment from 'moment'; + /** * I18n * @@ -13,10 +15,15 @@ export class I18n { - translations = {}; + _translations = {}; + _datetimeLocale = undefined; add(id, translation) { - this.translations[id] = translation; + if (!this._translations[id]) { + this._translations[id] = translation; + } else { + throw new Error('I18N Error: Attempting to set translation multiple times for »' + id + '«!'); + } } addMany(manyTranslations) { @@ -24,9 +31,27 @@ export class I18n { } get(id) { - if (!this.translations[id]) { + if (!this._translations[id]) { throw new Error('I18N Error: Translation missing for »' + id + '«!'); } - return this.translations[id]; + return this._translations[id]; + } + + + setDatetimeLocale(locale) { + if (!this._datetimeLocale) { + moment.locale(locale); + this._datetimeLocale = locale; + } else { + throw new Error('I18N Error: Attempting to set datetime locale multiple times!'); + } + } + + getDatetimeLocale() { + if (!this._datetimeLocale) { + throw new Error('I18N Error: Attempting to access datetime locale when it has not been set!'); + } + + return this._datetimeLocale; } } diff --git a/frontend/src/utils/form/datepicker.js b/frontend/src/utils/form/datepicker.js index 2cc4972c8..b8ba0869f 100644 --- a/frontend/src/utils/form/datepicker.js +++ b/frontend/src/utils/form/datepicker.js @@ -48,9 +48,6 @@ const DATEPICKER_CONFIG = { timeMinutes: 0, timeSeconds: 0, - // german settings - // TODO: hardcoded, get from current language / settings - locale: 'de', weekStart: 1, dateFormat: FORM_DATE_FORMAT_DATE_DT, timeFormat: FORM_DATE_FORMAT_TIME_DT, @@ -86,6 +83,7 @@ export class Datepicker { datepickerInstance; _element; elementType; + _locale; constructor(element) { if (!element) { @@ -96,6 +94,8 @@ export class Datepicker { return false; } + this._locale = window.App.i18n.getDatetimeLocale(); + // initialize datepickerCollections singleton if not already done if (!Datepicker.datepickerCollections) { Datepicker.datepickerCollections = new Map(); @@ -134,7 +134,7 @@ export class Datepicker { } // initialize tail.datetime (datepicker) instance and let it do weird stuff with the element value - this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig }); + this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig, locale: this._locale }); // reset date to something sane if (parsedMomentDate) diff --git a/messages/uniworx/de-de-formal.msg b/messages/uniworx/de-de-formal.msg index bf651eca0..364fcb8b4 100644 --- a/messages/uniworx/de-de-formal.msg +++ b/messages/uniworx/de-de-formal.msg @@ -2104,3 +2104,9 @@ MenuGlossary: Begriffsverzeichnis Applicant: Bewerber CourseParticipant: Kursteilnehmer +Administrator: Administrator +CsvFormat: CSV-Format +ExerciseSheet: Übungsblatt +DefinitionCourseEvents: Kurstermine +DefinitionCourseNews: Kurs-Aktuelles +Invitations: Einladungen \ No newline at end of file diff --git a/messages/uniworx/en-eu.msg b/messages/uniworx/en-eu.msg index f3a46e755..ce14e2845 100644 --- a/messages/uniworx/en-eu.msg +++ b/messages/uniworx/en-eu.msg @@ -1802,7 +1802,7 @@ ApplicationRatingSection: Grading ApplicationRatingSectionSelfTip: You are authorised to edit the application as well as it's grading. AllocationSchoolShort: Department -Allocation: Central alloction +Allocation: Central allocation AllocationRegisterTo: Registration until AllocationListTitle: Central allocations @@ -2100,3 +2100,9 @@ MenuGlossary: Glossary Applicant: Applicant CourseParticipant: Course participant +Administrator: Administrator +CsvFormat: CSV format +ExerciseSheet: Exercise sheet +DefinitionCourseEvents: Course occurrences +DefinitionCourseNews: Course news +Invitations: Invitations \ No newline at end of file diff --git a/src/Foundation.hs b/src/Foundation.hs index 34b7d6013..50d2b538d 100644 --- a/src/Foundation.hs +++ b/src/Foundation.hs @@ -1930,6 +1930,7 @@ siteLayout' headingOverride widget = do let -- See Utils.Frontend.I18n and files in messages/frontend for message definitions frontendI18n = toJSON (mr :: FrontendMessage -> Text) + frontendDatetimeLocale <- toJSON <$> selectLanguage frontendDatetimeLocales pc <- widgetToPageContent $ do -- fonts diff --git a/src/Handler/Info.hs b/src/Handler/Info.hs index c4a5fe7dc..69aa8437b 100644 --- a/src/Handler/Info.hs +++ b/src/Handler/Info.hs @@ -2,6 +2,7 @@ module Handler.Info where import Import import Handler.Utils +import Handler.Info.TH import qualified Data.Map as Map import qualified Data.CaseInsensitive as CI @@ -84,7 +85,4 @@ getGlossaryR = $(widgetFile "glossary") where entries = $(i18nWidgetFiles "glossary") - msgMap = Map.fromList - [ ("applicant" , MsgApplicant ) - , ("course-participant", MsgCourseParticipant) - ] + msgMap = $(glossaryTerms "glossary") diff --git a/src/Handler/Info/TH.hs b/src/Handler/Info/TH.hs new file mode 100644 index 000000000..25c55bdb6 --- /dev/null +++ b/src/Handler/Info/TH.hs @@ -0,0 +1,23 @@ +module Handler.Info.TH + ( glossaryTerms + ) where + +import Import +import Handler.Utils.I18n + +import Language.Haskell.TH + +import qualified Data.Char as Char + +import qualified Data.Map.Strict as Map +import qualified Data.Text as Text + + +glossaryTerms :: FilePath -> Q Exp +glossaryTerms basename = do + translationsAvailable <- i18nWidgetFilesAvailable' basename + let terms = Map.mapWithKey (\k _ -> "Msg" <> unPathPiece k) translationsAvailable + [e|Map.fromList $(listE . map (\(int, msg) -> tupE [litE . stringL $ repack int, conE $ mkName msg]) $ Map.toList terms)|] + where + unPathPiece :: Text -> String + unPathPiece = repack . mconcat . map (over _head Char.toUpper) . Text.splitOn "-" diff --git a/src/Handler/Utils/I18n.hs b/src/Handler/Utils/I18n.hs index f3b2e157a..a98067a49 100644 --- a/src/Handler/Utils/I18n.hs +++ b/src/Handler/Utils/I18n.hs @@ -1,10 +1,13 @@ module Handler.Utils.I18n - where + ( i18nWidgetFile + , i18nWidgetFilesAvailable, i18nWidgetFilesAvailable', i18nWidgetFiles + ) where import Import import Language.Haskell.TH import Language.Haskell.TH.Syntax (qRunIO) +import qualified Language.Haskell.TH.Syntax as TH import qualified Data.List as List import qualified Data.List.NonEmpty as NonEmpty @@ -47,8 +50,8 @@ i18nWidgetFile basename = do ] ++ [ clause [wildP] (normalB [e| error "selectLanguage returned an invalid translation" |]) [] ] -- Fallback mostly there so compiler does not complain about non-exhaustive pattern match ] [e|selectLanguage availableTranslations' >>= $(varE ws)|] -i18nWidgetFiles :: FilePath -> Q Exp -i18nWidgetFiles basename = do +i18nWidgetFilesAvailable' :: FilePath -> Q (Map Text (NonEmpty Text)) +i18nWidgetFilesAvailable' basename = do let i18nDirectory = "templates" > "i18n" > basename availableFiles <- qRunIO $ listDirectory i18nDirectory let fileKinds' = fmap (pack . dropExtension . takeBaseName &&& toTranslation . pack . takeBaseName) availableFiles @@ -58,6 +61,15 @@ i18nWidgetFiles basename = do availableTranslations' <- iforM fileKinds $ \kind -> maybe (fail $ "‘" <> i18nDirectory <> "’ has no translations for ‘" <> unpack kind <> "’") return . NonEmpty.nonEmpty + return availableTranslations' + +i18nWidgetFilesAvailable :: FilePath -> Q Exp +i18nWidgetFilesAvailable = TH.lift <=< i18nWidgetFilesAvailable' + +i18nWidgetFiles :: FilePath -> Q Exp +i18nWidgetFiles basename = do + availableTranslations' <- i18nWidgetFilesAvailable' basename + -- Dispatch to correct language (depending on user settings via `selectLanguage`) at run time ws <- newName "ws" -- Name for dispatch function letE diff --git a/src/Utils/Frontend/I18n.hs b/src/Utils/Frontend/I18n.hs index 9c8533496..1d1c24652 100644 --- a/src/Utils/Frontend/I18n.hs +++ b/src/Utils/Frontend/I18n.hs @@ -1,5 +1,6 @@ module Utils.Frontend.I18n ( FrontendMessage(..) + , frontendDatetimeLocales ) where import ClassyPrelude @@ -14,6 +15,9 @@ import Data.Aeson.Types (toJSONKeyText) import Data.Aeson.TH import qualified Data.Char as Char +import Data.List.NonEmpty (NonEmpty(..)) +import Text.Shakespeare.I18N (Lang) + -- | I18n-Messages used in JavaScript-Frontend -- @@ -39,3 +43,7 @@ instance ToJSONKey FrontendMessage where toJSONKey = toJSONKeyText toPathPiece instance FromJSONKey FrontendMessage where fromJSONKey = FromJSONKeyTextParser $ parseJSON . String + + +frontendDatetimeLocales :: NonEmpty Lang +frontendDatetimeLocales = "de" :| ["en"] diff --git a/src/Utils/Lang.hs b/src/Utils/Lang.hs index fe41e110f..3280c62fc 100644 --- a/src/Utils/Lang.hs +++ b/src/Utils/Lang.hs @@ -11,8 +11,6 @@ import qualified Data.List as List import qualified Data.CaseInsensitive as CI -import Control.Lens (none) - import Yesod.Core.Types (HandlerData(handlerRequest), YesodRequest(reqLangs)) import qualified Network.Wai.Parse as NWP @@ -41,7 +39,7 @@ selectLanguages avL (l:ls) , l'' <- matchesFor l' , langMatches lParts' l'' ] - = let now = nonEmpty . filter (\l' -> none (((==) `on` CI.mk) l') ls) $ sortOn (Down . length) found + = let now = nonEmpty $ sortOn (Down . length) found others = selectLanguages avL ls in maybe id (\now' others' -> NonEmpty.fromList $ toList now' ++ filter (`notElem` toList now') (toList others')) now others | otherwise = selectLanguages avL ls diff --git a/templates/glossary.cassius b/templates/glossary.cassius index 226288fc5..8a6bc42cf 100644 --- a/templates/glossary.cassius +++ b/templates/glossary.cassius @@ -11,4 +11,4 @@ margin-left: 12px dd + dt, .dd + dt, dd + .dt, .dd + .dt - margin-top: 17px \ No newline at end of file + margin-top: 17px diff --git a/templates/i18n.julius b/templates/i18n.julius index 97c088a81..690f8da89 100644 --- a/templates/i18n.julius +++ b/templates/i18n.julius @@ -1,6 +1,6 @@ if (window.App) { window.App.i18n.addMany(#{frontendI18n}); - // window.App.i18n.setLang(lang); TODO: set language string for datepicker config + window.App.i18n.setDatetimeLocale(#{frontendDatetimeLocale}); } else { throw new Error('I18n JavaScript service is missing!'); } diff --git a/templates/i18n/glossary/administrator.de-de-formal.hamlet b/templates/i18n/glossary/administrator.de-de-formal.hamlet new file mode 100644 index 000000000..b811f11f0 --- /dev/null +++ b/templates/i18n/glossary/administrator.de-de-formal.hamlet @@ -0,0 +1,7 @@ +$newline never +
+ Wenn eine Veranstaltung ein Bewerbungsverfahren hat, werden, statt sich als #
+ Student direkt für die Veranstaltung anzumelden, nur Bewerbungen an die #
+ Veranstaltung gestellt.
+ Es ist dann Aufgabe der Kursverwalter die Bewerbungen zu bewerten und ggf. zu #
+ akzeptieren.
+
+ Auch Veranstaltungen, die an einer Zentralanmeldung teilnehmen, können ein #
+ Bewerbungsverfahren haben oder nicht.
+ In diesem Fall werden immer Bewerbungen (über die Zentralanmeldung) an den #
+ Kurs gestellt, es werden jedoch die Einstellungen vom kurseigenen #
+ Bewerbungsverfahren übernommen (z.B. ob die Abgabe von Dateien erlaubt ist).
diff --git a/templates/i18n/glossary/course-application-template.de-de-formal.hamlet b/templates/i18n/glossary/course-application-template.de-de-formal.hamlet
new file mode 100644
index 000000000..3f5d05417
--- /dev/null
+++ b/templates/i18n/glossary/course-application-template.de-de-formal.hamlet
@@ -0,0 +1,8 @@
+$newline never
+
+ Modelliert jene Stellen der Universität, die Zugriff auf Prüfungsleistungen #
+ benötigen um sicherzustellen, dass Studienmodalitäten erfüllt und Leistungen #
+ korrekt anerkannt werden.
+
+ Teil des Prüfungsamts ist eine Berechtigung die einzelnen Benutzern von #
+ einem Administrator für ein bestimmtes Institut eingeräumt wird.
+ Diese Benutzer haben dann u.A. Zugriff auf alle beim relevanten Institut #
+ erbrachten und in Uni2work hinterlegten Prüfungsleistungen.
diff --git a/templates/i18n/glossary/school-lecturer.de-de-formal.hamlet b/templates/i18n/glossary/school-lecturer.de-de-formal.hamlet
new file mode 100644
index 000000000..90f2c92c7
--- /dev/null
+++ b/templates/i18n/glossary/school-lecturer.de-de-formal.hamlet
@@ -0,0 +1,4 @@
+$newline never
+