diff --git a/langs/en-ca.json b/langs/en-ca.json
index af3b8f9..751507c 100644
--- a/langs/en-ca.json
+++ b/langs/en-ca.json
@@ -7,6 +7,9 @@
"RipCrypt": {
"sheet-names": {
"HeroSummaryCardV1": "Hero Stat Card"
+ },
+ "common": {
+ "empty": "---"
}
}
}
diff --git a/module/handlebarHelpers/_index.mjs b/module/handlebarHelpers/_index.mjs
new file mode 100644
index 0000000..1603faf
--- /dev/null
+++ b/module/handlebarHelpers/_index.mjs
@@ -0,0 +1,11 @@
+import { handlebarsLocalizer, localizer } from "../utils/Localizer.mjs";
+import { options } from "./options.mjs";
+
+export default {
+ // #region Complex
+ "rc-i18n": handlebarsLocalizer,
+ "rc-options": options,
+
+ // #region Simple
+ "rc-empty-state": (v) => v ?? localizer(`RipCrypt.common.empty`),
+};
diff --git a/module/handlebarHelpers/options.mjs b/module/handlebarHelpers/options.mjs
new file mode 100644
index 0000000..e88ba34
--- /dev/null
+++ b/module/handlebarHelpers/options.mjs
@@ -0,0 +1,36 @@
+import { localizer } from "../utils/Localizer.mjs";
+
+/**
+ * @typedef {object} Option
+ * @property {string} [label]
+ * @property {string|number} value
+ * @property {boolean} [disabled]
+ */
+
+/**
+ * @param {string | number} selected
+ * @param {Array`,
+ );
+ };
+};
diff --git a/module/utils/Localizer.mjs b/module/utils/Localizer.mjs
new file mode 100644
index 0000000..1a92058
--- /dev/null
+++ b/module/utils/Localizer.mjs
@@ -0,0 +1,37 @@
+import { localizerConfig } from "../config.mjs";
+
+export function handlebarsLocalizer(key, ...args) {
+ let data = args[0];
+ if (args.length === 1) { data = args[0].hash };
+ if (key instanceof Handlebars.SafeString) { key = key.toString() };
+ const localized = localizer(key, data);
+ return localized;
+};
+
+export function localizer(key, args = {}, depth = 0) {
+ /** @type {string} */
+ let localized = game.i18n.format(key, args);
+ const subkeys = localized.matchAll(localizerConfig.subKeyPattern);
+
+ // Short-cut to help prevent infinite recursion
+ if (depth > localizerConfig.maxDepth) {
+ return localized;
+ };
+
+ /*
+ Helps prevent recursion on the same key so that we aren't doing excess work.
+ */
+ const localizedSubkeys = new Map();
+ for (const match of subkeys) {
+ const subkey = match.groups.key;
+ if (localizedSubkeys.has(subkey)) { continue };
+ localizedSubkeys.set(subkey, localizer(subkey, args, depth + 1));
+ };
+
+ return localized.replace(
+ localizerConfig.subKeyPattern,
+ (_fullMatch, subkey) => {
+ return localizedSubkeys.get(subkey);
+ },
+ );
+};