diff --git a/.changeset/large-rabbits-matter.md b/.changeset/large-rabbits-matter.md new file mode 100644 index 00000000..804b881e --- /dev/null +++ b/.changeset/large-rabbits-matter.md @@ -0,0 +1,6 @@ +--- +"@lookit/templates": patch +"@lookit/record": patch +--- + +Add ability to select specific video consent template. diff --git a/package-lock.json b/package-lock.json index 90f45c15..1aaba5a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17217,6 +17217,8 @@ }, "node_modules/rollup-plugin-dotenv": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-dotenv/-/rollup-plugin-dotenv-0.5.1.tgz", + "integrity": "sha512-ARUPDmeKAw3niZ2Ajv0qKNRryIWFMW796oJSS1hNdop3HF63Vljio/QRmG6ob0aQzzVUrFq6vW1p4jOE6xDQrQ==", "dev": true, "license": "MIT", "dependencies": { @@ -20331,6 +20333,7 @@ "devDependencies": { "@jspsych/config": "^2.0.0", "handlebars": "^4.7.8", + "rollup-plugin-dotenv": "^0.5.1", "rollup-plugin-polyfill-node": "^0.13.0" }, "peerDependencies": { diff --git a/packages/record/src/consentVideo.spec.ts b/packages/record/src/consentVideo.spec.ts index 1ad76c7e..fa048dd2 100644 --- a/packages/record/src/consentVideo.spec.ts +++ b/packages/record/src/consentVideo.spec.ts @@ -42,7 +42,10 @@ test("Trial", () => { const jsPsych = initJsPsych(); const plugin = new VideoConsentPlugin(jsPsych); const display = document.createElement("div"); - const trial = { locale: "en-us" } as unknown as TrialType; + const trial = { + locale: "en-us", + template: "consent-template-5", + } as unknown as TrialType; plugin["recordFeed"] = jest.fn(); plugin["recordButton"] = jest.fn(); @@ -116,7 +119,10 @@ test("onEnded", () => { const play = document.createElement("button"); const next = document.createElement("button"); const record = document.createElement("button"); - const trial = { locale: "en-us" } as unknown as TrialType; + const trial = { + locale: "en-us", + template: "consent-template-5", + } as unknown as TrialType; display.innerHTML = chsTemplates.consentVideo(trial); plugin["recordFeed"] = jest.fn(); @@ -152,7 +158,10 @@ test("getButton", () => { const jsPsych = initJsPsych(); const plugin = new VideoConsentPlugin(jsPsych); const display = document.createElement("div"); - const trial = { locale: "en-us" } as unknown as TrialType; + const trial = { + locale: "en-us", + template: "consent-template-5", + } as unknown as TrialType; display.innerHTML = chsTemplates.consentVideo(trial); @@ -182,7 +191,10 @@ test("recordButton", async () => { const jsPsych = initJsPsych(); const plugin = new VideoConsentPlugin(jsPsych); const display = document.createElement("div"); - const trial = { locale: "en-us" } as unknown as TrialType; + const trial = { + locale: "en-us", + template: "consent-template-5", + } as unknown as TrialType; display.innerHTML = chsTemplates.consentVideo(trial); @@ -228,7 +240,10 @@ test("playButton", () => { const jsPsych = initJsPsych(); const plugin = new VideoConsentPlugin(jsPsych); const display = document.createElement("div"); - const trial = { locale: "en-us" } as unknown as TrialType; + const trial = { + locale: "en-us", + template: "consent-template-5", + } as unknown as TrialType; plugin["playbackFeed"] = jest.fn(); @@ -249,7 +264,10 @@ test("stopButton", async () => { const jsPsych = initJsPsych(); const plugin = new VideoConsentPlugin(jsPsych); const display = document.createElement("div"); - const trial = { locale: "en-us" } as unknown as TrialType; + const trial = { + locale: "en-us", + template: "consent-template-5", + } as unknown as TrialType; display.innerHTML = chsTemplates.consentVideo(trial) + Handlebars.compile(recordFeed)({}); @@ -274,7 +292,10 @@ test("nextButton", () => { const jsPsych = initJsPsych(); const plugin = new VideoConsentPlugin(jsPsych); const display = document.createElement("div"); - const trial = { locale: "en-us" } as unknown as TrialType; + const trial = { + locale: "en-us", + template: "consent-template-5", + } as unknown as TrialType; display.innerHTML = chsTemplates.consentVideo(trial); plugin["endTrial"] = jest.fn(); diff --git a/packages/record/src/consentVideo.ts b/packages/record/src/consentVideo.ts index eeda729e..de1c40c2 100644 --- a/packages/record/src/consentVideo.ts +++ b/packages/record/src/consentVideo.ts @@ -16,7 +16,7 @@ const info = { name: "consent-video", version, parameters: { - template: { type: ParameterType.STRING, default: "consent_005" }, + template: { type: ParameterType.STRING, default: "consent-template-5" }, locale: { type: ParameterType.STRING, default: "en-us" }, additional_video_privacy_statement: { type: ParameterType.STRING, diff --git a/packages/templates/hbs/consent-document.hbs b/packages/templates/hbs/consent-template-5.hbs similarity index 100% rename from packages/templates/hbs/consent-document.hbs rename to packages/templates/hbs/consent-template-5.hbs diff --git a/packages/templates/package.json b/packages/templates/package.json index 7cd013a8..eb3c558d 100644 --- a/packages/templates/package.json +++ b/packages/templates/package.json @@ -27,6 +27,7 @@ "devDependencies": { "@jspsych/config": "^2.0.0", "handlebars": "^4.7.8", + "rollup-plugin-dotenv": "^0.5.1", "rollup-plugin-polyfill-node": "^0.13.0" }, "peerDependencies": { diff --git a/packages/templates/rollup.config.mjs b/packages/templates/rollup.config.mjs index b0ac4b28..63119b11 100644 --- a/packages/templates/rollup.config.mjs +++ b/packages/templates/rollup.config.mjs @@ -1,3 +1,4 @@ +import dotenv from "rollup-plugin-dotenv"; import nodePolyfills from "rollup-plugin-polyfill-node"; import { importAsString } from "rollup-plugin-string-import"; import { makeRollupConfig } from "../../rollup.mjs"; @@ -7,7 +8,8 @@ export default makeRollupConfig("chsTemplates").map((config) => { ...config, plugins: [ ...config.plugins, - + // Add support for .env files + dotenv(), // Add support to import yaml and handlebars files as strings importAsString({ include: ["**/*.yaml", "**/*.hbs"], diff --git a/packages/templates/src/consentVideo.ts b/packages/templates/src/consentVideoTemplate.ts similarity index 59% rename from packages/templates/src/consentVideo.ts rename to packages/templates/src/consentVideoTemplate.ts index 13d71412..63870f04 100644 --- a/packages/templates/src/consentVideo.ts +++ b/packages/templates/src/consentVideoTemplate.ts @@ -1,9 +1,10 @@ import { LookitWindow } from "@lookit/data/dist/types"; import Handlebars from "handlebars"; import { PluginInfo, TrialType } from "jspsych"; -import consentDocumentTemplate from "../hbs/consent-document.hbs"; +import consent_template_5 from "../hbs/consent-template-5.hbs"; import consentVideoTrialTemplate from "../hbs/consent-video-trial.hbs"; -import { initI18nAndTemplates } from "./utils"; +import { ConsentTemplateNotFound } from "./errors"; +import { setLocale } from "./utils"; declare const window: LookitWindow; @@ -19,7 +20,9 @@ export const consentVideo = (trial: TrialType) => { const experiment = window.chs.study.attributes; const { PIName, PIContact } = trial; - initI18nAndTemplates(trial); + setLocale(trial); + + const consentDocumentTemplate = consentDocument(trial); const consent = Handlebars.compile(consentDocumentTemplate)({ ...trial, @@ -34,3 +37,18 @@ export const consentVideo = (trial: TrialType) => { video_container_id, }); }; + +/** + * Get consent template by name. + * + * @param trial - Trial data including user supplied parameters. + * @returns Consent template + */ +const consentDocument = (trial: TrialType) => { + switch (trial.template) { + case "consent-template-5": + return consent_template_5; + default: + throw new ConsentTemplateNotFound(trial.template); + } +}; diff --git a/packages/templates/src/environment.d.ts b/packages/templates/src/environment.d.ts new file mode 100644 index 00000000..013eed40 --- /dev/null +++ b/packages/templates/src/environment.d.ts @@ -0,0 +1,9 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + DEBUG?: string; + } + } +} + +export {}; diff --git a/packages/templates/src/errors.ts b/packages/templates/src/errors.ts index c748365f..07ddaa58 100644 --- a/packages/templates/src/errors.ts +++ b/packages/templates/src/errors.ts @@ -1,11 +1,22 @@ -/** Error throw what specified language isn't found */ -export class TranslationNotFoundError extends Error { +/** Error thrown when specified language isn't found */ +export class LocaleNotFoundError extends Error { /** * This will be thrown when attempting to init i18n * * @param baseName - Language a2code with region */ public constructor(baseName: string) { - super(`"${baseName}" translation not found.`); + super(`"${baseName}" locale not found.`); + } +} +/** Error thrown when researcher selects template that isn't available. */ +export class ConsentTemplateNotFound extends Error { + /** + * This will let the researcher know that their template isn't found. + * + * @param template - Supplied name of consent template. + */ + public constructor(template: string) { + super(`Consent template "${template}" not found.`); } } diff --git a/packages/templates/src/index.spec.ts b/packages/templates/src/index.spec.ts index 807d9db8..1aaa6459 100644 --- a/packages/templates/src/index.spec.ts +++ b/packages/templates/src/index.spec.ts @@ -1,11 +1,15 @@ import { LookitWindow } from "@lookit/data/dist/types"; import { PluginInfo, TrialType } from "jspsych"; +import { ConsentTemplateNotFound } from "./errors"; import chsTemplate from "./index"; declare const window: LookitWindow; test("consent video", () => { - const trial = { locale: "en-us" } as unknown as TrialType; + const trial = { + locale: "en-us", + template: "consent-template-5", + } as unknown as TrialType; const name = "some name"; window.chs = { study: { @@ -25,7 +29,10 @@ test("consent video", () => { }); test("consent video in French", () => { - const trial = { locale: "fr" } as unknown as TrialType; + const trial = { + locale: "fr", + template: "consent-template-5", + } as unknown as TrialType; const name = "some name"; window.chs = { study: { @@ -43,3 +50,13 @@ test("consent video in French", () => { `Consentement à participer à la recherche:\n ${name}`, ); }); + +test("consent video with unknown template", () => { + const trial = { + locale: "en-us", + template: "not a real template name", + } as unknown as TrialType; + expect(() => chsTemplate.consentVideo(trial)).toThrow( + ConsentTemplateNotFound, + ); +}); diff --git a/packages/templates/src/index.ts b/packages/templates/src/index.ts index d3e03529..c0531b8c 100644 --- a/packages/templates/src/index.ts +++ b/packages/templates/src/index.ts @@ -1,3 +1,3 @@ -import { consentVideo } from "./consentVideo"; +import { consentVideo } from "./consentVideoTemplate"; export default { consentVideo }; diff --git a/packages/templates/src/utils.spec.ts b/packages/templates/src/utils.spec.ts index 6bd4e4ab..9747b3f0 100644 --- a/packages/templates/src/utils.spec.ts +++ b/packages/templates/src/utils.spec.ts @@ -1,15 +1,6 @@ -import Yaml from "js-yaml"; -import en_us from "../i18n/en-us.yaml"; -import eu from "../i18n/eu.yaml"; -import fr from "../i18n/fr.yaml"; -import hu from "../i18n/hu.yaml"; -import it from "../i18n/it.yaml"; -import ja from "../i18n/ja.yaml"; -import nl from "../i18n/nl.yaml"; -import pt_br from "../i18n/pt-br.yaml"; -import pt from "../i18n/pt.yaml"; -import { TranslationNotFoundError } from "./errors"; -import { expFormat, getTranslation } from "./utils"; +import { PluginInfo, TrialType } from "jspsych"; +import { LocaleNotFoundError } from "./errors"; +import { expFormat, setLocale } from "./utils"; test("expFormat convert written text to format well in HTML", () => { expect(expFormat("abcdefg")).toStrictEqual("abcdefg"); @@ -30,26 +21,7 @@ test("expFormat convert written text to format well in HTML", () => { ); }); -test("Get translation file for specified locale", () => { - const translations = { - ja, - pt, - eu, - fr, - hu, - it, - nl, - "en-us": en_us, - "pt-br": pt_br, - }; - - for (const [k, v] of Object.entries(translations)) { - expect(getTranslation(new Intl.Locale(k))).toStrictEqual(Yaml.load(v)); - } - - expect(pt_br).not.toStrictEqual(pt); - - expect(() => getTranslation(new Intl.Locale("not-a2code"))).toThrow( - TranslationNotFoundError, - ); +test("setLocale throw error with non-existing locale", () => { + const trial = { locale: "non-existing" } as unknown as TrialType; + expect(() => setLocale(trial)).toThrow(LocaleNotFoundError); }); diff --git a/packages/templates/src/utils.ts b/packages/templates/src/utils.ts index db894ddf..fb4a6913 100644 --- a/packages/templates/src/utils.ts +++ b/packages/templates/src/utils.ts @@ -12,7 +12,7 @@ import ja from "../i18n/ja.yaml"; import nl from "../i18n/nl.yaml"; import pt_br from "../i18n/pt-br.yaml"; import pt from "../i18n/pt.yaml"; -import { TranslationNotFoundError } from "./errors"; +import { LocaleNotFoundError } from "./errors"; /** * Pulled from EFP. Function to convert researcher's text to HTML. @@ -35,45 +35,32 @@ export const expFormat = (text?: string | string[]) => { }; /** - * Get a translation file based on selected language. + * Get a translation resources from yaml files. * - * @param lcl - Locale object with locale - * @returns Translations from i18next + * @returns Resources for i18next */ -export const getTranslation = (lcl: Intl.Locale) => { - /** - * Switch case to find language from a string. Will throw error is language - * not found. - * - * @param baseName - Base name from locale (en-us) - * @returns Language yaml file - */ - const getYaml = (baseName: string) => { - switch (baseName) { - case "en-US": - return en_us; - case "eu": - return eu; - case "fr": - return fr; - case "hu": - return hu; - case "it": - return it; - case "ja": - return ja; - case "nl": - return nl; - case "pt-BR": - return pt_br; - case "pt": - return pt; - default: - throw new TranslationNotFoundError(baseName); - } +const resources = () => { + const translations = { + "en-us": en_us, + eu, + fr, + hu, + it, + ja, + nl, + "pt-br": pt_br, + pt, }; - return Yaml.load(getYaml(lcl.baseName)) as Record; + return Object.entries(translations).reduce((prev, [locale, translation]) => { + const lcl = new Intl.Locale(locale); + return { + ...prev, + [lcl.baseName]: { + translation: Yaml.load(translation) as Record, + }, + }; + }, {}); }; /** @@ -81,39 +68,24 @@ export const getTranslation = (lcl: Intl.Locale) => { * * @param trial - Trial data including user supplied parameters. */ -const initI18next = (trial: TrialType) => { - const debug = process.env.DEBUG === "true"; +export const setLocale = (trial: TrialType) => { const lcl = new Intl.Locale(trial.locale); - const translation = getTranslation(lcl); - i18next.use(ICU).init({ - lng: lcl.baseName, - debug, - resources: { - [lcl.language]: { - translation, - }, - }, - }); -}; + if (!i18next.hasResourceBundle(lcl.baseName, "translation")) { + throw new LocaleNotFoundError(trial.locale); + } -/** - * Initialize handlebars helpers. This could be done globally, but it does go - * hand in hand with initializing i18n. - */ -const initHandlebars = () => { - Handlebars.registerHelper("t", (context, { hash }) => - i18next.t(context, hash), - ); - Handlebars.registerHelper("exp-format", (context) => expFormat(context)); + if (i18next.language !== lcl.baseName) { + i18next.changeLanguage(lcl.baseName); + } }; -/** - * Initialize both i18next and Handlebars. - * - * @param trial - Yup - */ -export const initI18nAndTemplates = (trial: TrialType) => { - initI18next(trial); - initHandlebars(); -}; +// Initialize translations +i18next.use(ICU).init({ + debug: process.env.DEBUG === "true", + resources: resources(), +}); + +// Setup Handlebars' helpers +Handlebars.registerHelper("exp-format", (context) => expFormat(context)); +Handlebars.registerHelper("t", (context, { hash }) => i18next.t(context, hash));