Skip to content

Commit

Permalink
feat(h5p-server): localization of library names (#1205)
Browse files Browse the repository at this point in the history
* feat(h5p-server): localization of library names

* feat(h5p-rest-example): added library localization to rest example
  • Loading branch information
sr258 committed Mar 21, 2021
1 parent 50c4813 commit dfbb892
Show file tree
Hide file tree
Showing 16 changed files with 368 additions and 62 deletions.
7 changes: 4 additions & 3 deletions docs/advanced/localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ shows where this must be done:
| 2. notify H5P editor client | Call `H5PEditor.render(contentId, language, ...)` with the language code you need. |
| 3. properties of IIntegration | Pass a valid `translationCallback` of type `ITranslationFunction` to the constructor of `H5PEditor` |
| 4. error messages emitted by @lumieducation/h5p-server | Catch errors of types `H5PError` and `AggregateH5PError` and localize the message property yourself. |
| 5. H5P Hub | When constructing `H5PEditor` set the option `enableHubLocalization` to true and load the namespace `hub` in your localization system. Call `H5PEditor.getContentTypeCache()` with a language or make sure that `req.language` is set in the get AJAX route when using `h5p-express`. |
| 5. H5P Hub | When constructing `H5PEditor` set the option `enableHubLocalization` to true and load the namespace `hub` in your localization system. Call `H5PEditor.getContentTypeCache()` with a language or make sure that `req.language` is set in the GET AJAX route when using `h5p-express`. |
| 6. library selector | When constructing `H5PEditor` set the option `enableLibraryNameLocalization` to true and load the namespace `library-metadata` in your localization system. Call `H5PEditor.getLibraryOverview()` with a language or make sure that `req.language` is set in the POST AJAX route when using `h5p-express`. |

The [Express example](/packages/h5p-examples/src/express.ts) demonstrates how to
do 1,2 and 3. The [Express adapter for the Ajax endpoints](/packages/h5p-express/src/H5PAjaxRouter/H5PAjaxExpressRouter.ts)
Expand All @@ -45,8 +46,8 @@ The language strings used by @lumieducation/h5p-server all follow the
conventions of [i18next](https://www.npmjs.com/package/i18next) and it is a good
library to perform the translation for cases 3 and 4. However, you are free to
use whatever translation library you want as long as you make sure to pass a
valid `translationCallback` to `H5PEditor` (case 3+5) and add the required
`t(...)` function to `req` (case 4).
valid `translationCallback` to `H5PEditor` (case 3, 5 and 6) and add the
required `t(...)` function to `req` (case 4).

### Initializing the JavaScript H5P client (in the browser)

Expand Down
File renamed without changes.
3 changes: 2 additions & 1 deletion packages/h5p-examples/src/createH5PEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ export default async function createH5PEditor(
translationCallback,
undefined,
{
enableHubLocalization: true
enableHubLocalization: true,
enableLibraryNameLocalization: true
}
);

Expand Down
23 changes: 13 additions & 10 deletions packages/h5p-examples/src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const start = async (): Promise<void> => {
'client',
'copyright-semantics',
'hub',
'library-metadata',
'metadata-semantics',
'mongo-s3-content-storage',
's3-temporary-storage',
Expand All @@ -65,7 +66,9 @@ const start = async (): Promise<void> => {

// Load the configuration file from the local file system
const config = await new H5P.H5PConfig(
new H5P.fsImplementations.JsonStorage(path.resolve('src/config.json'))
new H5P.fsImplementations.JsonStorage(
path.join(__dirname, '../config.json')
)
).load();

// The H5PEditor object is central to all operations of h5p-nodejs-library
Expand All @@ -80,12 +83,12 @@ const start = async (): Promise<void> => {
// H5P.fs(...).
const h5pEditor: H5P.H5PEditor = await createH5PEditor(
config,
path.resolve('h5p/libraries'), // the path on the local disc where
path.join(__dirname, '../h5p/libraries'), // the path on the local disc where
// libraries should be stored)
path.resolve('h5p/content'), // the path on the local disc where content
path.join(__dirname, '../h5p/content'), // the path on the local disc where content
// is stored. Only used / necessary if you use the local filesystem
// content storage class.
path.resolve('h5p/temporary-storage'), // the path on the local disc
path.join(__dirname, '../h5p/temporary-storage'), // the path on the local disc
// where temporary files (uploads) should be stored. Only used /
// necessary if you use the local filesystem temporary storage class.
(key, language) => translationFunction(key, { lng: language })
Expand Down Expand Up @@ -148,9 +151,9 @@ const start = async (): Promise<void> => {
h5pEditor.config.baseUrl,
h5pAjaxExpressRouter(
h5pEditor,
path.resolve('h5p/core'), // the path on the local disc where the
path.resolve(path.join(__dirname, '../h5p/core')), // the path on the local disc where the
// files of the JavaScript client of the player are stored
path.resolve('h5p/editor'), // the path on the local disc where the
path.resolve(path.join(__dirname, '../h5p/editor')), // the path on the local disc where the
// files of the JavaScript client of the editor are stored
undefined,
'auto' // You can change the language of the editor here by setting
Expand All @@ -169,7 +172,7 @@ const start = async (): Promise<void> => {
expressRoutes(
h5pEditor,
h5pPlayer,
'auto' // You can change the language of the editor here by setting
'auto' // You can change the language of the editor by setting
// the language code you need here. 'auto' means the route will try
// to use the language detected by the i18next language detector.
)
Expand All @@ -193,8 +196,8 @@ const start = async (): Promise<void> => {
h5pEditor.libraryStorage,
h5pEditor.contentStorage,
h5pEditor.config,
path.resolve('h5p/core'),
path.resolve('h5p/editor')
path.join(__dirname, '../h5p/core'),
path.join(__dirname, '../h5p/editor')
);

server.get('/h5p/html/:contentId', async (req, res) => {
Expand All @@ -213,7 +216,7 @@ const start = async (): Promise<void> => {
// buttons to display, edit, delete and download existing content.
server.get('/', startPageRenderer(h5pEditor));

server.use('/client', express.static(path.resolve('build/client')));
server.use('/client', express.static(path.join(__dirname, 'client')));

// STUB, not implemented yet. You have to get the user id through a session
// cookie as h5P does not add it to the request. Alternatively you could add
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default class H5PAjaxExpressController {
req.query.machineName as string,
req.query.majorVersion as string,
req.query.minorVersion as string,
(req as any).language ?? (req.query.language as string),
(req.query.language as string) ?? (req as any).language,
req.user
);
res.status(200).send(result);
Expand Down Expand Up @@ -169,7 +169,7 @@ export default class H5PAjaxExpressController {
const result = await this.ajaxEndpoint.postAjax(
req.query.action as string,
req.body as any,
req.query.language as string,
(req.query.language as string) ?? (req as any).language,
req.user,
req.files?.file,
req.query.id as string,
Expand Down
3 changes: 2 additions & 1 deletion packages/h5p-rest-example-server/src/createH5PEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ export default async function createH5PEditor(
translationCallback,
undefined,
{
enableHubLocalization: true
enableHubLocalization: true,
enableLibraryNameLocalization: true
}
);

Expand Down
1 change: 1 addition & 0 deletions packages/h5p-rest-example-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const start = async (): Promise<void> => {
'client',
'copyright-semantics',
'hub',
'library-metadata',
'metadata-semantics',
'mongo-s3-content-storage',
's3-temporary-storage',
Expand Down
63 changes: 63 additions & 0 deletions packages/h5p-server/assets/translations/library-metadata/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"H5P_Image": {
"title": "Bild"
},
"H5P_ContinuousText": {
"title": "Fortlaufender Text"
},
"H5P_FreeTextQuestion": {
"title": "Freitexteingabe"
},
"H5P_GoToScene": {
"title": "Gehe zu Szene"
},
"H5P_GoToQuestion": {
"title": "Verzweigung"
},
"H5P_GoalsPage": {
"title": "Ziele-Seite"
},
"H5P_GoalsAssessmentPage": {
"title": "Zielbewertungs-Seite"
},
"H5P_StandardPage": {
"title": "Standard-Seite"
},
"H5P_DocumentExportPage": {
"title": "Dokumenten-Export-Seite"
},
"H5P_Link": {
"title": "Link"
},
"H5P_Nil": { "title": "Beschriftung" },
"H5P_IVHotspot": {
"title": "Navigations-Button"
},
"H5P_OpenEndedQuestion": {
"title": "Offene Frage"
},
"H5P_Shape": {
"title": "Formen"
},
"H5P_Summary": {
"title": "Zusammenfassung"
},
"H5P_Table": {
"title": "Tabelle"
},
"H5P_Text": {
"title": "Text"
},
"H5P_Video": {
"title": "Video"
},
"H5P_AdvancedText": {
"title": "Text"
},
"H5P_SimpleMultiChoice": {
"title": "Einfaches Multiple-Choice"
},
"H5P_BranchingQuestion": {
"title": "Verzweigungsfrage"
}
}
63 changes: 63 additions & 0 deletions packages/h5p-server/assets/translations/library-metadata/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"H5P_Image": {
"title": "Image"
},
"H5P_ContinuousText": {
"title": "Continuous Text"
},
"H5P_FreeTextQuestion": {
"title": "Free Text Question"
},
"H5P_GoToScene": {
"title": "Go To Scene"
},
"H5P_GoToQuestion": {
"title": "Crossroads"
},
"H5P_GoalsPage": {
"title": "Goals page"
},
"H5P_GoalsAssessmentPage": {
"title": "Goals assessment page"
},
"H5P_StandardPage": {
"title": "Standard page"
},
"H5P_DocumentExportPage": {
"title": "Document Export Page"
},
"H5P_Link": {
"title": "Link"
},
"H5P_Nil": { "title": "Label" },
"H5P_IVHotspot": {
"title": "Navigation Hotspot"
},
"H5P_OpenEndedQuestion": {
"title": "Open Ended Question"
},
"H5P_Shape": {
"title": "Shapes"
},
"H5P_Summary": {
"title": "Summary"
},
"H5P_Table": {
"title": "Table"
},
"H5P_Text": {
"title": "Text"
},
"H5P_Video": {
"title": "Video"
},
"H5P_AdvancedText": {
"title": "Text"
},
"H5P_SimpleMultiChoice": {
"title": "Simple Multi Choice"
},
"H5P_BranchingQuestion": {
"title": "Branching Question"
}
}
43 changes: 15 additions & 28 deletions packages/h5p-server/src/ContentTypeInformationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from './types';

import Logger from './helpers/Logger';
import TranslatorWithFallback from './helpers/TranslatorWithFallback';

const log = new Logger('ContentTypeInformationRepository');

Expand Down Expand Up @@ -46,11 +47,18 @@ export default class ContentTypeInformationRepository {
private contentTypeCache: ContentTypeCache,
private libraryManager: LibraryManager,
private config: IH5PConfig,
private translationCallback?: ITranslationFunction
translationCallback?: ITranslationFunction
) {
log.info(`initialize`);
if (translationCallback) {
this.translator = new TranslatorWithFallback(translationCallback, [
'hub'
]);
}
}

private translator: TranslatorWithFallback;

/**
* Gets the information about available content types with all the extra
* information as listed in the class description.
Expand All @@ -59,7 +67,7 @@ export default class ContentTypeInformationRepository {
log.info(`getting information about available content types`);
let cachedHubInfo = await this.contentTypeCache.get();
if (
this.translationCallback &&
this.translator &&
language &&
language.toLowerCase() !== 'en' && // We don't localize English as the base strings already are in English
!language.toLowerCase().startsWith('en-')
Expand Down Expand Up @@ -323,7 +331,7 @@ export default class ContentTypeInformationRepository {
contentTypes: IHubContentType[],
language: string
): IHubContentType[] {
if (!this.translationCallback) {
if (!this.translator) {
throw new Error(
'You need to instantiate ContentTypeInformationRepository with a translationCallback if you want to localize Hub information.'
);
Expand All @@ -333,18 +341,18 @@ export default class ContentTypeInformationRepository {
const cleanMachineName = ct.machineName.replace('.', '_');
return {
...ct,
summary: this.tryLocalize(
summary: this.translator.tryLocalize(
`${cleanMachineName}.summary`,
ct.summary,
language
),
description: this.tryLocalize(
description: this.translator.tryLocalize(
`${cleanMachineName}.description`,
ct.description,
language
),
keywords: ct.keywords.map((kw) =>
this.tryLocalize(
this.translator.tryLocalize(
`${ct.machineName.replace(
'.',
'_'
Expand All @@ -353,33 +361,12 @@ export default class ContentTypeInformationRepository {
language
)
),
title: this.tryLocalize(
title: this.translator.tryLocalize(
`${cleanMachineName}.title`,
ct.title,
language
)
};
});
}

/**
* Tries localizing the entry of the content type information. If it fails
* (indicated by the fact that the key is part of the localized string), it
* will return the original source string.
* @param key the key to look up the translation in the i18n data
* @param sourceString the original English string received from the Hub
* @param language the desired language
* @returns the localized string or the original English source string
*/
private tryLocalize(
key: string,
sourceString: string,
language: string
): string {
const localized = this.translationCallback(`hub:${key}`, language);
if (localized.includes(key)) {
return sourceString;
}
return localized;
}
}
10 changes: 7 additions & 3 deletions packages/h5p-server/src/H5PAjaxEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,8 +422,9 @@ export default class H5PAjaxEndpoint {
* installs the libraries in it; returns the parameters
* and metadata in it
* @param body the parsed JSON content of the request body
* @param language (needed for 'translations') the language code for which
* the translations should be retrieved, e.g. 'en'. This paramter is part
* @param language (needed for 'translations' and optionally possible for
* 'libraries') the language code for which the translations should be
* retrieved, e.g. 'en'. This paramter is part
* of the query URL, e.g. POST /ajax?action=translations&language=en
* @param user (needed for 'files' and 'library-install') the user who is
* performing the action. It is the job of the implementation to inject this
Expand Down Expand Up @@ -515,7 +516,10 @@ export default class H5PAjaxEndpoint {
}
// getLibraryOverview validates the library list, so we don't do
// it here.
return this.h5pEditor.getLibraryOverview(body.libraries);
return this.h5pEditor.getLibraryOverview(
body.libraries,
language
);
case 'translations':
if (!('libraries' in body) || !Array.isArray(body.libraries)) {
throw new H5pError(
Expand Down
Loading

0 comments on commit dfbb892

Please sign in to comment.