Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(h5p-server): hub now localizable #1200

Merged
merged 5 commits into from
Mar 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/advanced/localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ Some places of H5P cannot be localized at this time (this must be changed by
Joubel):

* Some strings in libraries that are hard-coded
* The H5P Hub content list and description

### Changing the language of the editor

Expand All @@ -35,17 +34,18 @@ 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`. |

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)
do 1,2 and 3. The [Express adapter for the Ajax endpoints](/packages/h5p-express/src/H5PAjaxRouter/H5PAjaxExpressRouter.ts)
already implements 4 but requires the `t(...)` function to be added to the `req`
object.

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) and add the required
valid `translationCallback` to `H5PEditor` (case 3+5) and add the required
`t(...)` function to `req` (case 4).

### Initializing the JavaScript H5P client (in the browser)
Expand Down
6 changes: 5 additions & 1 deletion packages/h5p-examples/src/createH5PEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,11 @@ export default async function createH5PEditor(
: new H5P.fsImplementations.DirectoryTemporaryFileStorage(
localTemporaryPath
),
translationCallback
translationCallback,
undefined,
{
enableHubLocalization: true
}
);

// Set bucket lifecycle configuration for S3 temporary storage to make
Expand Down
1 change: 1 addition & 0 deletions packages/h5p-examples/src/express.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const start = async (): Promise<void> => {
ns: [
'client',
'copyright-semantics',
'hub',
'metadata-semantics',
'mongo-s3-content-storage',
's3-temporary-storage',
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.query.language as string,
(req as any).language ?? (req.query.language as string),
req.user
);
res.status(200).send(result);
Expand Down
6 changes: 5 additions & 1 deletion packages/h5p-rest-example-server/src/createH5PEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,11 @@ export default async function createH5PEditor(
: new H5P.fsImplementations.DirectoryTemporaryFileStorage(
localTemporaryPath
),
translationCallback
translationCallback,
undefined,
{
enableHubLocalization: true
}
);

// Set bucket lifecycle configuration for S3 temporary storage to make
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 @@ -50,6 +50,7 @@ const start = async (): Promise<void> => {
ns: [
'client',
'copyright-semantics',
'hub',
'metadata-semantics',
'mongo-s3-content-storage',
's3-temporary-storage',
Expand Down
325 changes: 325 additions & 0 deletions packages/h5p-server/assets/translations/hub/de.json

Large diffs are not rendered by default.

403 changes: 403 additions & 0 deletions packages/h5p-server/assets/translations/hub/en.json

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions packages/h5p-server/scripts/create-hub-base-locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fsExtra from 'fs-extra';
import path from 'path';

const hubData = fsExtra.readJSONSync(
path.resolve(
path.join(
__dirname,
'../../../test/data/content-type-cache/real-content-types.json'
)
)
);

const reducedHubData = hubData.contentTypes.reduce((prev, ct) => {
prev[ct.id.replace('.', '_')] = {
title: `${ct.title} (${ct.title})`,
summary: ct.summary,
description: ct.description,
keywords: ct.keywords?.reduce((prev, curr) => {
prev[curr.replace(' ', '_')] = curr;
return prev;
}, {})
};
return prev;
}, {});

fsExtra.writeJSONSync(
path.resolve(path.join(__dirname, '../assets/translations/hub/en.json')),
reducedHubData,
{
spaces: 4
}
);
96 changes: 92 additions & 4 deletions packages/h5p-server/src/ContentTypeInformationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
IHubInfo,
IInstalledLibrary,
ILibraryInstallResult,
ITranslationFunction,
IUser
} from './types';

Expand All @@ -36,21 +37,35 @@ export default class ContentTypeInformationRepository {
* @param contentTypeCache
* @param libraryManager
* @param config
* @param translationCallback (optional) if passed in, the object will try
* to localize content type information (if a language is passed to the
* `get(...)` method). You can safely leave it out if you don't want to
* localize hub information.
*/
constructor(
private contentTypeCache: ContentTypeCache,
private libraryManager: LibraryManager,
private config: IH5PConfig
private config: IH5PConfig,
private translationCallback?: ITranslationFunction
) {
log.info(`initialize`);
}

/**
* Gets the information about available content types with all the extra information as listed in the class description.
* Gets the information about available content types with all the extra
* information as listed in the class description.
*/
public async get(user: IUser): Promise<IHubInfo> {
public async get(user: IUser, language?: string): Promise<IHubInfo> {
log.info(`getting information about available content types`);
const cachedHubInfo = await this.contentTypeCache.get();
let cachedHubInfo = await this.contentTypeCache.get();
if (
this.translationCallback &&
language &&
language.toLowerCase() !== 'en' && // We don't localize English as the base strings already are in English
!language.toLowerCase().startsWith('en-')
) {
cachedHubInfo = this.localizeHubInfo(cachedHubInfo, language);
}
let hubInfoWithLocalInfo = await this.addUserAndInstallationSpecificInfo(
cachedHubInfo,
user
Expand Down Expand Up @@ -294,4 +309,77 @@ export default class ContentTypeInformationRepository {
}
return library.restricted;
}

/**
* Returns a transformed list of content type information in which the
* visible strings are localized into the desired language. Only works if
* the namespace 'hub' has been initialized and populated by the i18n
* system.
* @param contentTypes
* @param language
* @returns the transformed list of content types
*/
private localizeHubInfo(
contentTypes: IHubContentType[],
language: string
): IHubContentType[] {
if (!this.translationCallback) {
throw new Error(
'You need to instantiate ContentTypeInformationRepository with a translationCallback if you want to localize Hub information.'
);
}

return contentTypes.map((ct) => {
const cleanMachineName = ct.machineName.replace('.', '_');
return {
...ct,
summary: this.tryLocalize(
`${cleanMachineName}.summary`,
ct.summary,
language
),
description: this.tryLocalize(
`${cleanMachineName}.description`,
ct.description,
language
),
keywords: ct.keywords.map((kw) =>
this.tryLocalize(
`${ct.machineName.replace(
'.',
'_'
)}.keywords.${kw.replace('_', ' ')}`,
kw,
language
)
),
title: this.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;
}
}
8 changes: 5 additions & 3 deletions packages/h5p-server/src/H5PAjaxEndpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default class H5PAjaxEndpoint {
'You must specify a user when calling getAjax(...).'
);
}
return this.h5pEditor.getContentTypeCache(user);
return this.h5pEditor.getContentTypeCache(user, language);
case 'libraries':
if (
machineName === undefined ||
Expand Down Expand Up @@ -650,7 +650,8 @@ export default class H5PAjaxEndpoint {
).length;

const contentTypeCache = await this.h5pEditor.getContentTypeCache(
user
user,
language
);
return new AjaxSuccessResponse(
contentTypeCache,
Expand Down Expand Up @@ -685,7 +686,8 @@ export default class H5PAjaxEndpoint {
).length;

const contentTypes = await this.h5pEditor.getContentTypeCache(
user
user,
language
);
return new AjaxSuccessResponse(
{
Expand Down
15 changes: 10 additions & 5 deletions packages/h5p-server/src/H5PEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ export default class H5PEditor {
this.contentTypeRepository = new ContentTypeInformationRepository(
this.contentTypeCache,
this.libraryManager,
config
config,
options?.enableHubLocalization ? translationCallback : undefined
);
this.temporaryFileManager = new TemporaryFileManager(
temporaryStorage,
Expand Down Expand Up @@ -305,12 +306,16 @@ export default class H5PEditor {
}

/**
* Returns the content type cache for a specific user. This includes all available content types for the user (some
* might be restricted) and what the user can do with them (update, install from Hub).
* Returns the content type cache for a specific user. This includes all
* available content types for the user (some might be restricted) and what
* the user can do with them (update, install from Hub).
*/
public getContentTypeCache(user: IUser): Promise<IHubInfo> {
public getContentTypeCache(
user: IUser,
language?: string
): Promise<IHubInfo> {
log.info(`getting content type cache`);
return this.contentTypeRepository.get(user);
return this.contentTypeRepository.get(user, language);
}

/**
Expand Down
15 changes: 15 additions & 0 deletions packages/h5p-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,21 @@ export interface IH5PEditorOptions {
styles?: string[];
};
};
/**
* If true, the system will localize the information about content types
* displayed in the H5P Hub. It will use the translationCallback that is
* passed to H5PEditor for this by getting translations from the namespace
* 'hub'. It will try to localize these language strings:
* hub:H5P_Example.description
* hub:H5P_Example.summary
* hub:H5P_Example.keywords.key_word1
* hub:H5P_Example.keywords.key_word2
* hub:H5P_Example.keywords. ...
* Note that "H5P_Example" is a transformed version of the machineName of
* the content type main library, in which . is replaced by _. In the key
* words whitespaces are replaced by _.
*/
enableHubLocalization?: boolean;
}

/**
Expand Down
Loading