Skip to content

Extension Development 'How To' Guide

jolierabideau edited this page Apr 25, 2025 · 11 revisions

As an extension developer, how do I ...


Add a new application setting

Click here for details
  1. Locate the settings contribution file for your extension. This is identified by the settings value in your manifest.json file. Typically, it is settings.json in your contributions folder.

  2. Add an object into the array for each setting that you want to add. Here is the general format to use:

    {
      "label": "%localizeKey_for_settings_group_name_to_display_in_settings_ui%",
      "description": "%localizeKey_for_settings_group_description_to_display_in_settings_ui%",
      "properties": {
        "<my extension name>.<my setting name>": {
          "label": "%localizeKey_for_setting_name_to_display_in_settings_ui%",
          "description": "%localizeKey_for_setting_description_to_display_in_settings_ui%",
          "default": <default value for this setting>,
          "derivesFrom": <optional name of other setting that will be used as the starting value of this setting the first time it is loaded>
        }
      }
    }

    For example, the following could be the data for a setting called "colorScheme" in an extension named "myExtension":

    {
      "label": "%myExtension_setting_group_name%",
      "description": "%myExtension_setting_group_description%",
      "properties": {
        "myExtension.colorScheme": {
          "label": "%myExtension_setting_color_scheme%",
          "description": "%myExtension_setting_color_scheme_user_description%",
          "default": "standard"
        }
      }
    }
  3. Provide localization values for all of the LocalizeKey values in the labels and descriptions from your settings.json file. See the How To about localizing strings for more information. You should provide localization values for English (en) in addition to any other languages you are targeting.

  4. In your extension's d.ts file, declare all of the settings you defined in step 2.

    export interface SettingTypes {
      /** Color scheme used by myExtension */
      'myExtension.colorScheme': string;
    }
  5. In your extension's activate() function, add a validator for each setting to determine whether a new value is valid. Also, add the validator to context.registrations so it will be removed if your extension is deactivated.

    const colorSchemeValidationPromise = papi.settings.registerValidator(
      'myExtension.colorScheme',
      async (newValue) => typeof newValue === 'string' && (newValue as string).length > 0,
    );
    
    context.registrations.add(await colorSchemeValidationPromise);

NOTE: Settings apply to your extension regardless of what project a user has open. If you want a different value to be configurable on a per project basis, use a project setting instead.


Use application settings in React frontend

Click here for details
  1. In your web-view.tsx file, import the useSetting hook from the PAPI.

    import { useSetting } from '@papi/frontend/react'
  2. Call the hook with the setting key of the setting you want to retrieve or set and a default value. If the default value is not a boolean, integer, or string it should be wrapped in a useMemo to be made stable. The hook returns [setting, setSetting, resetSetting, isLoading].

    • setting is the current state of the setting, either defaultState, the stored value, or a PlatformError if loading the value fails. See Determine if a value is of type PlatformError
    • setSetting is a function that updates the setting to a new value.
    • resetSetting is a function that removes the setting and resets the value to defaultState from the settings contribution.
    • isLoading is a boolean that tells you if the value of the setting is still loading, so you can wait for the setting to load and create responsive UI's.
    const [setting, setSetting, resetSetting, isLoading] = useSetting(
        'helloWorld.personName',
        'Kathy',
    );

Now you can use and manipulate the application setting with setting, setSetting, and resetSetting.


Use application settings (non-React)

Click here for details

We can use and change application settings throughout the platform with the following functions, and a setting key, which is the string id of the setting for which the value is being get, set, or subscribed to.

  1. Import the papi object.

    • In HTML web-view:
    const papi = require('@papi/frontend');
    • In backend:
    import { papi } from '@papi/backend';
  2. To get a setting value, call papi.settings.get with the relevant setting key. This function returns the value of the specified setting, parsed to an object. Returns default setting if setting does not exist.

       const settingValue = await papi.settings.get('helloWorld.personName');
  3. To set a setting value, call papi.settings.set with the relevant setting key, and the value you would like to set it to. This function returns information that the PAPI uses to interpret whether to send out updates. Defaults to true (meaning send updates only for this data type).

       const updateInstructions = await papi.settings.set('helloWorld.personName', 'Ben');
  4. To reset a setting to its default value, call papi.settings.reset with the relevant setting key. This function returns true if successfully reset the project setting. false otherwise.

       const isResetSuccessful = await papi.settings.reset('helloWorld.personName');
  5. To subscribe to a setting, call papi.settings.subscribe. You need to pass it the setting key, a callback function that will be called whenever the specified setting is updated, and optional subscriber options that adjust how the subscriber emits updates. If there is an error while retrieving the updated data, the callback will run with a PlatformError instead of the data. See Determine if a value is of type PlatformError. This function returns an Unsubscriber that should be called whenever the subscription should be deleted.

       let name;
       const unsubscriber = await papi.settings.subscribe(
          'helloWorld.personName', 
          (newValue: string | PlatformError) => { 
             if (!isPlatformError(newValue)) name = newValue;
          },
          { retrieveDataImmediately: false, whichUpdates: '*' }
       )

Add a new project setting

Click here for details
  1. Locate the project settings contribution file for your extension. This is identified by the projectSettings value in your manifest.json file. Typically, it is projectSettings.json in your contributions folder.

  2. Add an object into the array for each project setting that you want to add. Here is the general format to use:

    {
      "label": "%localizeKey_for_project_settings_group_name_to_display_in_settings_ui%",
      "description": "%localizeKey_for_project_settings_group_description_to_display_in_settings_ui%",
      "properties": {
        "<my extension name>.<my setting name>": {
          "label": "%localizeKey_for_project_setting_name_to_display_in_settings_ui%",
          "description": "%localizeKey_for_project_setting_description_to_display_in_settings_ui%",
          "default": <default value for this setting>,
          "includeProjectInterfaces": <optional regular expressions identifying which projects this setting applies to>,
          "excludeProjectInterfaces": <optional regular expressions identifying which projects this setting does not apply to>,
          "includePdpFactoryIds": <optional regular expressions identifying which projects this setting applies to>,
          "excludePdpFactoryIds": <optional regular expressions identifying which projects this setting does not apply to>
        }
      }
    }

    For example, the following could be the data for a project setting called "specialFeatureEnabled" in an extension named "myExtension":

    {
      "label": "%myExtension_project_setting_group_name%",
      "description": "%myExtension_project_setting_group_description%",
      "properties": {
        "myExtension.specialFeatureEnabled": {
          "label": "%myExtension_project_setting_special_feature%",
          "description": "%myExtension_project_setting_special_feature_user_description%",
          "default": false,
          "includeProjectInterfaces": ["Scripture"]
        }
      }
    }

    If you want to limit your extension to working with Scripture projects, you will want to specify that as a value for includeProjectInterfaces. If you don't want to limit your extension to particular types of projects, then you don't need to specify a value. Using includePdpFactoryIds, excludePdpFactoryIds, and excludeProjectInterfaces values is advanced and not necessary in most cases.

    Here is more information about:

    If you are working with custom project types you created, then you will need to understand project interfaces in more detail.  

  3. Provide localization values for all of the LocalizeKey values in the labels and descriptions from your projectSettings.json file. See the How To about localizing strings for more information. You should provide localization values for English (en) in addition to any other languages you are targeting.

  4. In your extension's d.ts file, declare all of the settings you defined in step 2.

    export interface ProjectSettingTypes {
      /** Determines whether "special feature" is enabled within myExtension */
      'myExtension.specialFeatureEnabled': boolean;
    }
  5. In your extension's activate() function, add a validator for each project setting to determine whether a new value is valid. Also, add the validator to context.registrations so it will be removed if your extension is deactivated.

    const specialFeaturePromise = papi.projectSettings.registerValidator(
      'myExtension.specialFeatureEnabled',
      async (newValue) => typeof newValue === 'boolean',
    );
    
    context.registrations.add(await specialFeaturePromise);

Use project settings in React frontend

Click here for details
  1. In your web-view.tsx file, import the useProjectSetting hook from the PAPI.

    import { useProjectSetting } from '@papi/frontend/react'
  2. Call the hook with the project data provider source of the project (relevant projectId), the setting key of the setting you want to retrieve or set, and a default value. If the default value is not a boolean, integer, or string it should be wrapped in a useMemo to be made stable. The hook returns [setting, setSetting, resetSetting, isLoading].

    • setting is the current state of the setting, either defaultState, the stored value, or a PlatformError if loading the value fails. See Determine if a value is of type PlatformError.
    • setSetting is a function that updates the setting to a new value.
    • resetSetting is a function that removes the setting and resets the value to defaultState from the settings contribution.
    • isLoading is a boolean that tells you if the value of the setting is still loading, so you can wait for the setting to load and create responsive UI's.
    const [setting, setSetting, resetSetting, isLoading] = useProjectSetting(
        projectId,
        'helloWorld.headerColor',
        'Red',
    );

Now you can use and manipulate the project setting with setting, setSetting, and resetSetting.


Add menu items

Click here for details
  1. Locate the menu contribution file for your extension. This is identified by the menus value in your manifest.json file. Typically, it is menus.json in your contributions folder.

  2. Update the menu contribution file with the menu items you want to add.

    Here is a default, empty contribution:

    {
      "mainMenu": {
        "columns": {},
        "groups": {},
        "items": []
      },
      "defaultWebViewTopMenu": {
        "columns": {},
        "groups": {},
        "items": []
      },
      "defaultWebViewContextMenu": {
        "groups": {},
        "items": []
      },
      "webViewMenus": {}
    }

TODO: UPDATE THIS SECTION ONCE WE HAVE SETTLED ON EXTENSIBILITY POINTS FOR MENUS.


Localize strings to another language in the UI

Click here for details
  1. Select the target locale for localizing values. The locale will need to be identified as a BCP 47 language tag. Normally a 2 letter language code (e.g., pt for Portuguese) would be a good choice. However, if you are trying to localize for a particular language variant that could meaningfully different between members of the same language community, you might want to be more specific (e.g., pt-BR for Brazilian Portuguese).

  2. Identify the LocalizeKey values that you want to localize. These will all be of the form %_____%. That is, they all start and end with a percentage sign (%). If you are adding UI in your extension, the values will be used in your code. If you want to see all LocalizeKey values currently loaded by the application and their associated localized strings, do the following:

    • Select Help > Open Developer Documentation in the application menu. A web browser should open to "Live PAPI documentation".
    • Click on the INSPECTOR to open a pane at the bottom of the browser.
    • Paste the following in the box on left side:
      {
        "jsonrpc": "2.0",
        "method": "object:platform.localizationDataServiceDataProvider-data.retrieveCurrentLocalizedStringData",
        "params": [],
        "id": 1000
      }
    • Click the "Play" button immediately above the box where you pasted the JSON value in the previous step.
    • Copy value of result in the returned payload.
  3. Locate the localized string contribution file for your extension. This is identified by the localizedStrings value in your manifest.json file. Typically, it is localizedStrings.json in your contributions folder.

  4. Create a section in the localizedStrings file for your selected locale. Enter the localized values. For example:

    {
      "metadata": {},
      "localizedStrings": {
        "fr": {
          "%general_open%": "Ouvrir",
        }
      }
    }

NOTE: Localized strings are all part of a global namespace in the application. If you provide a localization value for a given LocalizeKey, that localization value will be used everywhere in the application that uses the same LocalizeKey. It doesn't matter what code (e.g., the core platform, your extension, a different extension, etc.) is displaying the string.


Add a new type of data

Click here for details

If you want extensions (yours and others) to be able to call useData to read/write data that your extension controls, you need to create a DataProvider and register it with the application when it starts up.

Here is an example of how you could do this for a service in an extension named myExtension that defines a data type called Footnote that is an array of strings tied to a ScriptureReference.

  1. In your extension's d.ts file, create a type that defines the named data types you want to expose.

    /**
     * Data types for reading and writing custom footnote data
     */
    export type FootnoteProviderDataTypes = {
      Footnotes: DataProviderDataType<ScriptureReference, string[], string[]>;
    };

The first generic parameter of DataProviderDataType is the "selector". It is some sort of identifier for indexing into the data in your DataProvider. The second generic parameter is the data type you will return via a get method. The third generic parameter is the data type you will accept via a set method. If your data provider won't allow getting or setting a particular data type, you can specify the parameter as never.

  1. In the same d.ts file, create a type that uses the new type from the previous step with the IDataProvider interface and includes any documentation you want users to see.

    /**
     * Service that reads and writes custom footnote data
     *
     * Use the "myExtension.footnotes" data provider name to access the service.
     */
    export type IFootnoteEnhancementService = IDataProvider<FootnoteProviderDataTypes>;
  2. In the same d.ts file, declare your data provider name.

    export interface DataProviders {
      /** Use this to work with custom footnotes */
      'myExtension.footnotes': IFootnoteEnhancementService;
    }
  3. Create a class that derives from DataProviderEngine and implements the type from step 1. The return values of your set functions indicate whether subscribers need to call get again to refresh to the latest data. If your data provider doesn't allow set or get calls, you can throw an error in the body of the function. However, you must define a get and set call for each data type.

    class FootnotesDataProviderEngine
      extends DataProviderEngine<FootnoteProviderDataTypes>
      implements IDataProviderEngine<FootnoteProviderDataTypes>
    {
      async getFootnotes(scriptureReference: ScriptureReference): Promise<string[]> {
        // body of function
      }
    
      async setFootnotes(scriptureReference: ScriptureReference, footnotesToSave: string[]): Promise<DataProviderUpdateInstructions<FootnoteProviderDataTypes>> {
        // body of function
      }
    }
  4. Register the data provider engine using the same name as declared in step 3.

    const dataProvider = await papi.dataProviders.registerEngine('myExtension.footnotes', new FootnotesDataProviderEngine());
  5. Ensure that when your extension is deactivated, your data provider is disposed. This can be done easily by adding it to context.registrations in your activate() function.

    context.registrations.add(dataProvider);

Launch a new process

Click here for details
  1. Modify your manifest.json file to add createProcess to the elevatedPrivileges array.

    "elevatedPrivileges": ["createProcess"],
  2. Make sure you declare the ExecutionActivationContext parameter of your activate() function. This should be in your main.ts file (or whatever other file you defined as "main" in your manifest.json).

    export async function activate(context: ExecutionActivationContext) {
  3. Update your activate function to call fork or spawn. Note that if createProcess is not included in your manifest.json file, then context.elevatedPrivileges.createProcess will be undefined.

    if (context.elevatedPrivileges.createProcess)
      context.elevatedPrivileges.createProcess.spawn(context.executionToken, 'commandToRun', ['arg1', 'arg2'], { stdio: ['pipe', 'pipe', 'pipe'] });

Note: Any process directly created using elevatedPrivileges.createProcess will be terminated when the application closes. If you create any additional processes from within these processes, those "second level processes" will not be automatically terminated.

Note: Remember that the application runs on Windows, macOS, and Linux. If you need to differentiate what you run based on OS, you can use createProcess.osData to determine which OS you are running on.


Manage other extensions

Click here for details
  1. Modify your manifest.json file to add manageExtensions to the elevatedPrivileges array.

    "elevatedPrivileges": ["manageExtensions"],
  2. Make sure you declare the ExecutionActivationContext parameter of your activate() function. This should be in your main.ts file (or whatever other file you defined as "main" in your manifest.json).

    export async function activate(context: ExecutionActivationContext) {
  3. Add code to manage extensions using context.elevatedPrivileges.manageExtensions functions as appropriate. Note that if manageExtensions is not included in your manifest.json file, then context.elevatedPrivileges.manageExtensions will be undefined.


Determine if a value is of type PlatformError

Click here for details

PlatformError is an error type with stronger typing of properties than Error. It is used to represent errors that are returned by the platform. You can see this error when subscribing to data from various sources including project data and settings. You can check if a value is a PlatformError object using isPlatformError(). This function accepts a value and returns true if the value is a PlatformError and false otherwise.

  1. Import isPlatformError() from the platform-bible-utils library.

       import { isPlatformError } from 'platform-bible-utils`;
  2. Check if a value is a PlatformError, in this example we use a setting value.

       const [setting, setSetting] = useSetting('helloWorld.personName', 'Kathy');
       if (isPlatformError(setting)) return '';

Are there other things you would like to see in this list? Feel free to create a new issue and describe what you think would be helpful!