@lookit/templates 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ import { PluginInfo, TrialType } from "jspsych";
2
+ /**
3
+ * Pulled from EFP. Function to convert researcher's text to HTML.
4
+ *
5
+ * @param text - Text
6
+ * @returns Formatted string
7
+ */
8
+ export declare const expFormat: (text?: string | string[]) => string;
9
+ /**
10
+ * Initialize i18next with parameters from trial.
11
+ *
12
+ * @param trial - Trial data including user supplied parameters.
13
+ */
14
+ export declare const setLocale: (trial: TrialType<PluginInfo>) => void;
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@lookit/templates",
3
+ "version": "0.0.1",
4
+ "description": "CHS jsPsych trial templates and their translations",
5
+ "homepage": "https://github.com/lookit/lookit-jspsych#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/lookit/lookit-jspsych/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/lookit/lookit-jspsych.git"
12
+ },
13
+ "license": "ISC",
14
+ "author": "Christopher J Green <okaycj@mit.edu> (https://github.com/okaycj)",
15
+ "main": "dist/index.js",
16
+ "unpkg": "dist/index.browser.min.js",
17
+ "types": "./dist/index.d.ts",
18
+ "files": [
19
+ "src",
20
+ "dist"
21
+ ],
22
+ "scripts": {
23
+ "build": "rollup --config",
24
+ "dev": "rollup --config rollup.config.dev.mjs --watch",
25
+ "test": "jest --coverage"
26
+ },
27
+ "devDependencies": {
28
+ "@jspsych/config": "^2.0.0",
29
+ "handlebars": "^4.7.8",
30
+ "rollup-plugin-dotenv": "^0.5.1",
31
+ "rollup-plugin-polyfill-node": "^0.13.0"
32
+ },
33
+ "peerDependencies": {
34
+ "@lookit/data": "^0.0.4",
35
+ "jspsych": "^8.0.2"
36
+ }
37
+ }
@@ -0,0 +1,57 @@
1
+ import { LookitWindow } from "@lookit/data/dist/types";
2
+ import Handlebars from "handlebars";
3
+ import { PluginInfo, TrialType } from "jspsych";
4
+ import consent_garden from "../hbs/consent-garden.hbs";
5
+ import consent_template_5 from "../hbs/consent-template-5.hbs";
6
+ import consentVideoTrialTemplate from "../hbs/consent-video-trial.hbs";
7
+ import { ConsentTemplateNotFound } from "./errors";
8
+ import { setLocale } from "./utils";
9
+
10
+ declare const window: LookitWindow;
11
+
12
+ const video_container_id = "lookit-jspsych-video-container";
13
+
14
+ /**
15
+ * Translate, render, and get consent document HTML.
16
+ *
17
+ * @param trial - JsPsych trial object containing trial params
18
+ * @returns Consent document HTML
19
+ */
20
+ export const consentVideo = (trial: TrialType<PluginInfo>) => {
21
+ const experiment = window.chs.study.attributes;
22
+ const { PIName, PIContact } = trial;
23
+
24
+ setLocale(trial);
25
+
26
+ const consentDocumentTemplate = consentDocument(trial);
27
+
28
+ const consent = Handlebars.compile(consentDocumentTemplate)({
29
+ ...trial,
30
+ name: PIName,
31
+ contact: PIContact,
32
+ experiment,
33
+ });
34
+
35
+ return Handlebars.compile(consentVideoTrialTemplate)({
36
+ ...trial,
37
+ consent,
38
+ video_container_id,
39
+ });
40
+ };
41
+
42
+ /**
43
+ * Get consent template by name.
44
+ *
45
+ * @param trial - Trial data including user supplied parameters.
46
+ * @returns Consent template
47
+ */
48
+ const consentDocument = (trial: TrialType<PluginInfo>) => {
49
+ switch (trial.template) {
50
+ case "consent-template-5":
51
+ return consent_template_5;
52
+ case "consent-garden":
53
+ return consent_garden;
54
+ default:
55
+ throw new ConsentTemplateNotFound(trial.template);
56
+ }
57
+ };
@@ -0,0 +1,9 @@
1
+ declare global {
2
+ namespace NodeJS {
3
+ interface ProcessEnv {
4
+ DEBUG?: string;
5
+ }
6
+ }
7
+ }
8
+
9
+ export {};
package/src/errors.ts ADDED
@@ -0,0 +1,22 @@
1
+ /** Error thrown when specified language isn't found */
2
+ export class LocaleNotFoundError extends Error {
3
+ /**
4
+ * This will be thrown when attempting to init i18n
5
+ *
6
+ * @param baseName - Language a2code with region
7
+ */
8
+ public constructor(baseName: string) {
9
+ super(`"${baseName}" locale not found.`);
10
+ }
11
+ }
12
+ /** Error thrown when researcher selects template that isn't available. */
13
+ export class ConsentTemplateNotFound extends Error {
14
+ /**
15
+ * This will let the researcher know that their template isn't found.
16
+ *
17
+ * @param template - Supplied name of consent template.
18
+ */
19
+ public constructor(template: string) {
20
+ super(`Consent template "${template}" not found.`);
21
+ }
22
+ }
@@ -0,0 +1,74 @@
1
+ import { LookitWindow } from "@lookit/data/dist/types";
2
+ import { PluginInfo, TrialType } from "jspsych";
3
+ import { ConsentTemplateNotFound } from "./errors";
4
+ import chsTemplate from "./index";
5
+
6
+ declare const window: LookitWindow;
7
+
8
+ /**
9
+ * Test helper function to create trial object.
10
+ *
11
+ * @param values - Object to replace default trial values
12
+ * @returns Trial object
13
+ */
14
+ const getTrial = (values: Record<string, string> = {}) => {
15
+ return {
16
+ locale: "en-us",
17
+ template: "consent-template-5",
18
+ ...values,
19
+ } as unknown as TrialType<PluginInfo>;
20
+ };
21
+
22
+ test("consent video", () => {
23
+ const trial = getTrial();
24
+ const name = "some name";
25
+ window.chs = {
26
+ study: {
27
+ attributes: {
28
+ name,
29
+ duration: "duration",
30
+ },
31
+ },
32
+ } as typeof window.chs;
33
+
34
+ expect(chsTemplate.consentVideo(trial)).toContain(
35
+ '<div id="consent-video-trial">',
36
+ );
37
+ expect(chsTemplate.consentVideo(trial)).toContain(
38
+ `Consent to participate in research:\n ${name}`,
39
+ );
40
+ });
41
+
42
+ test("consent video in French", () => {
43
+ const trial = getTrial({ locale: "fr" });
44
+ const name = "some name";
45
+ window.chs = {
46
+ study: {
47
+ attributes: {
48
+ name,
49
+ duration: "duration",
50
+ },
51
+ },
52
+ } as typeof window.chs;
53
+
54
+ expect(chsTemplate.consentVideo(trial)).toContain(
55
+ '<div id="consent-video-trial">',
56
+ );
57
+ expect(chsTemplate.consentVideo(trial)).toContain(
58
+ `Consentement à participer à la recherche:\n ${name}`,
59
+ );
60
+ });
61
+
62
+ test("consent video with unknown template", () => {
63
+ const trial = getTrial({
64
+ template: "not a real template name",
65
+ });
66
+ expect(() => chsTemplate.consentVideo(trial)).toThrow(
67
+ ConsentTemplateNotFound,
68
+ );
69
+ });
70
+
71
+ test("consent garden template", () => {
72
+ const trial = getTrial({ template: "consent-garden" });
73
+ expect(chsTemplate.consentVideo(trial)).toContain("Project GARDEN");
74
+ });
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { consentVideo } from "./consentVideoTemplate";
2
+
3
+ export default { consentVideo };
@@ -0,0 +1,14 @@
1
+ declare module "*.svg" {
2
+ const file: string;
3
+ export default file;
4
+ }
5
+
6
+ declare module "*.yaml" {
7
+ const file: string;
8
+ export default file;
9
+ }
10
+
11
+ declare module "*.hbs" {
12
+ const file: string;
13
+ export default file;
14
+ }
@@ -0,0 +1,27 @@
1
+ import { PluginInfo, TrialType } from "jspsych";
2
+ import { LocaleNotFoundError } from "./errors";
3
+ import { expFormat, setLocale } from "./utils";
4
+
5
+ test("expFormat convert written text to format well in HTML", () => {
6
+ expect(expFormat("abcdefg")).toStrictEqual("abcdefg");
7
+ expect(expFormat("AAABBBCCC")).toStrictEqual("AAABBBCCC");
8
+ expect(expFormat("A normal sentence with multiple words.")).toStrictEqual(
9
+ "A normal sentence with multiple words.",
10
+ );
11
+ expect(expFormat(["Array", "of", "strings"])).toStrictEqual(
12
+ "Array<br><br>of<br><br>strings",
13
+ );
14
+ expect(expFormat("carriage return an newline\r\n")).toStrictEqual(
15
+ "carriage return an newline<br>",
16
+ );
17
+ expect(expFormat("new line\n")).toStrictEqual("new line<br>");
18
+ expect(expFormat("carriage return\r")).toStrictEqual("carriage return<br>");
19
+ expect(expFormat("\tTabbed text")).toStrictEqual(
20
+ "&nbsp;&nbsp;&nbsp;&nbsp;Tabbed text",
21
+ );
22
+ });
23
+
24
+ test("setLocale throw error with non-existing locale", () => {
25
+ const trial = { locale: "non-existing" } as unknown as TrialType<PluginInfo>;
26
+ expect(() => setLocale(trial)).toThrow(LocaleNotFoundError);
27
+ });
package/src/utils.ts ADDED
@@ -0,0 +1,94 @@
1
+ import Handlebars from "handlebars";
2
+ import i18next from "i18next";
3
+ import ICU from "i18next-icu";
4
+ import Yaml from "js-yaml";
5
+ import { PluginInfo, TrialType } from "jspsych";
6
+ import en_us from "../i18n/en-us.yaml";
7
+ import eu from "../i18n/eu.yaml";
8
+ import fr from "../i18n/fr.yaml";
9
+ import hu from "../i18n/hu.yaml";
10
+ import it from "../i18n/it.yaml";
11
+ import ja from "../i18n/ja.yaml";
12
+ import nl from "../i18n/nl.yaml";
13
+ import pt_br from "../i18n/pt-br.yaml";
14
+ import pt from "../i18n/pt.yaml";
15
+ import { LocaleNotFoundError } from "./errors";
16
+
17
+ /**
18
+ * Pulled from EFP. Function to convert researcher's text to HTML.
19
+ *
20
+ * @param text - Text
21
+ * @returns Formatted string
22
+ */
23
+ export const expFormat = (text?: string | string[]) => {
24
+ if (!text) {
25
+ return "";
26
+ }
27
+
28
+ if (Array.isArray(text)) {
29
+ text = text.join("\n\n");
30
+ }
31
+
32
+ return text
33
+ .replace(/(\r\n|\n|\r)/gm, "<br>")
34
+ .replace(/\t/gm, "&nbsp;&nbsp;&nbsp;&nbsp;");
35
+ };
36
+
37
+ /**
38
+ * Get a translation resources from yaml files.
39
+ *
40
+ * @returns Resources for i18next
41
+ */
42
+ const resources = () => {
43
+ const translations = {
44
+ "en-us": en_us,
45
+ eu,
46
+ fr,
47
+ hu,
48
+ it,
49
+ ja,
50
+ nl,
51
+ "pt-br": pt_br,
52
+ pt,
53
+ };
54
+
55
+ return Object.entries(translations).reduce((prev, [locale, translation]) => {
56
+ const lcl = new Intl.Locale(locale);
57
+ return {
58
+ ...prev,
59
+ [lcl.baseName]: {
60
+ translation: Yaml.load(translation) as Record<string, string>,
61
+ },
62
+ };
63
+ }, {});
64
+ };
65
+
66
+ /**
67
+ * Initialize i18next with parameters from trial.
68
+ *
69
+ * @param trial - Trial data including user supplied parameters.
70
+ */
71
+ export const setLocale = (trial: TrialType<PluginInfo>) => {
72
+ const lcl = new Intl.Locale(trial.locale);
73
+
74
+ if (!i18next.hasResourceBundle(lcl.baseName, "translation")) {
75
+ throw new LocaleNotFoundError(trial.locale);
76
+ }
77
+
78
+ if (i18next.language !== lcl.baseName) {
79
+ i18next.changeLanguage(lcl.baseName);
80
+ }
81
+ };
82
+
83
+ // Initialize translations
84
+ i18next.use(ICU).init({
85
+ debug: process.env.DEBUG === "true",
86
+ resources: resources(),
87
+ });
88
+
89
+ // Setup Handlebars' helpers
90
+ Handlebars.registerHelper("exp-format", (context) => expFormat(context));
91
+ Handlebars.registerHelper("t", (context, { hash }) => {
92
+ const txt = String(i18next.t(context, hash));
93
+ return hash.htmlSafe ? new Handlebars.SafeString(txt) : txt;
94
+ });