/**
* Provides the read functionality to read language content in the
* item definition, module, or even the root context
*
* @module
*/
import React, { useContext } from "react";
import { LocaleContext, ILocaleContextType } from "../../internal/providers/locale-provider";
import { ItemContext } from "../../providers/item";
import { localeReplacerToArray, localeReplacer } from "../../../util";
import { ModuleContext } from "../../providers/module";
import { capitalize } from "../../../util";
import Module from "../../../base/Root/Module";
import ItemDefinition from "../../../base/Root/Module/ItemDefinition";
import Include from "../../../base/Root/Module/ItemDefinition/Include";
import RootRetriever, { useRootRetriever } from "../root/RootRetriever";
import PropertyDefinition from "../../../base/Root/Module/ItemDefinition/PropertyDefinition";
/**
* For a given object target, it will loop until it gets a match for the given key
* @param target the object targe
* @param keySplitted the splitted key to dive in
*/
function loopForKeyAtTarget(target: any, keySplitted: string[]) {
let result = typeof target === "undefined" ? null : target;
keySplitted.forEach((key) => {
if (
result === null ||
typeof result === "string"
) {
result = null;
return;
}
result = typeof result[key] === "undefined" ? null : result[key];
});
return result;
}
export interface I18nReadOptions {
/**
* The id to read, very context dependent
*
* if propertyId is specified it will use the "properties" context for that property first
* if policy type and policy name specified it will use the context for such as well
* if nothing found (or no propertyId or policy) it will go into the next context
*
* if in an include context it will read from the include context itself for its standard
* localized properties, if nothing found it will go into the next context
*
* if in an item definition context (normal, extended, or search mode) it will read the item definition
* from the base context, note that custom, properties do not need to be "custom.myid" they can
* just be "myid" if nothing found (or no context) it will go to the next context
*
* if in a module context it will rea from the module base context, same rule applies for custom
* as item definition, if nothing found (or no module context) it will go to the next context
*
* the root context is the last, and reads from the base root properties or the main i18n data properties
* if nothing found in this last context, an error is thrown
*/
i18nId: string;
/**
* A context to override the current pass a registry key
*/
context?: string;
/**
* A property id to use as context
*/
propertyId?: string;
/**
* A policy type to use as context, must go along policy name
*/
policyType?: string;
/**
* A policy name to use as context, must go along policy type
*/
policyName?: string;
/**
* An include to use as context
*/
include?: string;
/**
* Arbitrary parameters to replace dynamic i18n strings
* these can be plain string, for simple replacement or literal react
* objects, using react objects will produce a react node as output
* rather than a string
*/
args?: any[];
/**
* used to wrap the content bits that are not part of the args
* since the args can be anything and they can be wrapped react components
* or strings, this will affect anything that is not the args if set
*/
argsContentBitsWrapper?: (text: string) => React.ReactNode;
/**
* Dangerous!... whether the content represents html instead of a plain string
* does not mix well with args if the output generated is a react node that
* does not serialize
*/
html?: boolean;
/**
* The wrapping tag for using in the html mode, by default is a div
*/
htmlWrappingTag?: string;
/**
* Whether to capitalize the output
*/
capitalize?: boolean;
/**
* Override for the whitespace style property
*/
whiteSpace?: string;
/**
* Use for countables if countable is defined the first
* numeric argument of the arg list will be used as the counted value
* in order to determine which counted element is to be used
* for display
*
* if you want to manually specify a count, use the countValue
* prop
*/
countable?: boolean;
/**
* By default when a field is countable the first numeric arg is to be used
* for the counter, use this to override that
*/
countValue?: number;
/**
* By default when dealing with numbers as args it will attempt to use
* Intl.NumberFormat to determine how they are to be displayed
*/
disableNumberFormat?: boolean;
/**
* By default the number formatting is done in the language
* that is specified by the current locale, use this to override this
* behaviour and set the language manually
*/
numberFormatLang?: string;
/**
* What to use for the number formatting options
*/
numberFormatOptions?: Intl.NumberFormatOptions;
}
/**
* The read props which takes the following data
*/
export interface II18nReadProps extends I18nReadOptions {
/**
* The actually value is passed in this function, if required
* otherwise it's just rendered
*/
children?: (value: React.ReactNode) => React.ReactNode;
}
/**
* This function is a helper that represents what is used internally by
* the i18n reader, given what it needs
*
* @param localeContext it needs the locale context to get the current language and the root locale data
* @param mod it needs the module in its context or null
* @param idef it needs the item definition in its context or null
* @param include the include in its context or null
* @param props the current props
*/
function i18nReadInternal(
localeContext: ILocaleContextType,
mod: Module,
idef: ItemDefinition,
include: Include,
props: II18nReadProps,
) {
const idSplitted = props.i18nId.split(".");
// so first we go in order of priority of what we want to read
let i18nValue: React.ReactNode = null;
// first by the inlcude context
if (include) {
// if we have a name we use the include context using the name i18n function
// as the name can be inherited from the item definition if not specified
if (props.propertyId) {
const property = include.getSinkingPropertyFor(props.propertyId);
i18nValue = loopForKeyAtTarget(
property.getI18nDataFor(localeContext.language),
idSplitted,
);
} else if (props.i18nId === "name") {
i18nValue = include.getI18nNameFor(localeContext.language) || null;
} else {
// othewise we just extract the i18n data for the include and call it with the id,
// normally there are only specific labels here at this level in the include context
i18nValue = loopForKeyAtTarget(
include.getI18nDataFor(localeContext.language),
idSplitted,
);
}
}
// so if the include thing failed and we have an item definition context
if (idef && i18nValue === null) {
if (props.propertyId) {
let property: PropertyDefinition;
if (idef.isInSearchMode()) {
try {
property = idef.getPropertyDefinitionFor(props.propertyId, true);
} catch {
property = idef.getStandardCounterpart().getPropertyDefinitionFor(props.propertyId, true);
}
} else {
property = idef.getPropertyDefinitionFor(props.propertyId, true);
}
i18nValue = loopForKeyAtTarget(
property.getI18nDataFor(localeContext.language),
idSplitted,
);
} else {
// so we get the i18n item definition data
const i18nIdefData =
idef.getI18nDataFor(localeContext.language);
// if we are specifying a policy like if we are in a policy context
if (props.policyType && props.policyName) {
// we go for the policy value and the policy name value
i18nValue = loopForKeyAtTarget(
i18nIdefData,
["policies", props.policyType, props.policyName].concat(idSplitted),
)
} else {
const customValue = loopForKeyAtTarget(
i18nIdefData,
["custom"].concat(idSplitted),
);
// otherwise without policy we check if we have custom data in the item definition
// and this custom properties fit the data
if (customValue) {
i18nValue = customValue;
} else {
// otherwise we give an standard property
i18nValue = loopForKeyAtTarget(
i18nIdefData,
idSplitted,
);
}
}
}
}
// now in modules
if (mod && i18nValue === null) {
if (props.propertyId) {
const property = mod.getPropExtensionFor(props.propertyId);
i18nValue = loopForKeyAtTarget(
property.getI18nDataFor(localeContext.language),
idSplitted,
);
} else {
// modules act similar to item definitions they also support custom properties
const i18nModData =
mod.getI18nDataFor(localeContext.language);
const customValue = loopForKeyAtTarget(
i18nModData,
["custom"].concat(idSplitted),
);
if (customValue) {
i18nValue = customValue;
} else {
i18nValue = loopForKeyAtTarget(
i18nModData,
idSplitted,
);
}
}
}
// now we search in the generic locale file, there are no custom, even though
// they are all required
if (i18nValue === null) {
// for that we extract it
i18nValue = loopForKeyAtTarget(
localeContext.i18n,
[localeContext.language].concat(idSplitted),
);
}
// for handling countables
if (i18nValue && props.countable) {
const countValue = (typeof props.countValue === "number" ? props.countValue :
(props.args && props.args.find((a) => typeof a === "number"))) || 0;
const sourceI18n = i18nValue;
const defaultCountValue = i18nValue["n"];
i18nValue = i18nValue[countValue.toString()];
if (!i18nValue) {
const matchingRange = Object.keys(sourceI18n).find((r) => {
if (r.includes("-")) {
const range = r.split("-").map(parseInt);
return (range[0] >= countValue && countValue >= range[1]);
}
return false;
});
if (!matchingRange) {
i18nValue = defaultCountValue;
} else {
i18nValue = sourceI18n[matchingRange];
}
}
}
// if we still find nothing in all these contexts
if (i18nValue === null || typeof i18nValue !== "string") {
// we want to throw an error
let errMessage: string = "Unknown key in context: " + props.i18nId;
// let's make the error more specific
if (idef) {
// specify the context
errMessage += "; in item definition context for " +
idef.getName();
if (mod) {
errMessage += "; in mod context for " + mod.getName();
}
// add the policies if any
if (props.policyType && props.policyName) {
errMessage += "; in policy " + props.policyType + " " + props.policyName;
}
// and the include is if so deemed required
if (include) {
errMessage += "; in item context for " +
include.getId();
}
// and the include is if so deemed required
if (props.propertyId) {
errMessage += "; for property id " +
props.propertyId;
}
}
if (typeof i18nValue !== "string") {
if (i18nValue === null) {
errMessage += "; did not resolve";
} else {
errMessage += "; resolves to an object instead of a string";
}
}
// throw the error
if (typeof window !== "undefined") {
console.error(errMessage);
}
i18nValue = "???";
}
// if we are passing arguments to replace the {0} {1} etc... numbers
if (props.args) {
// pass to the number formatter
const newArgs = (props.disableNumberFormat ? props.args : props.args.map((v) => {
if (typeof v === "number") {
return (new Intl.NumberFormat(props.numberFormatLang || localeContext.language, props.numberFormatOptions)).format(v);
};
return v;
}));
// we have two options, these are for basic types, which is faster and returns a string
if (
!props.argsContentBitsWrapper &&
newArgs.every((a) => typeof a === "string" || typeof a === "number" || a === null || typeof a === "undefined")
) {
// the standard locale replacer
i18nValue = localeReplacer(i18nValue as string, ...newArgs);
if (props.capitalize) {
i18nValue = capitalize(i18nValue as string);
}
} else {
if (props.capitalize) {
i18nValue = capitalize(i18nValue as string);
}
// otherwise we use the locale replacer to array which creates react fragments
// and returns an array of react nodes, this all depends on the args that the user
// is passing
i18nValue = localeReplacerToArray(i18nValue as string, ...newArgs).map((output, index) => {
// eg 0 2 4 all odd numbers are content bits
const isContentBit = index % 2 === 0;
const outputNode = isContentBit && props.argsContentBitsWrapper ? props.argsContentBitsWrapper(output) : output;
if (typeof output === "string") {
return (
{outputNode}
);
}
return (
{outputNode}
);
});
}
} else if (props.capitalize) {
i18nValue = capitalize(i18nValue as string);
}
// now we want to get the final node that we are returning, the react node
let finalContent = i18nValue;
if (props.html) {
const Element: any = props.htmlWrappingTag || "div";
finalContent = (
);
}
// if we haven't specified a children function
// we just return the entire content
if (!props.children) {
return finalContent;
}
// otherwise we return the children wrapped function
return props.children(finalContent);
}
/**
* For a pure component optimized class so that
* there are no useless re-renders when the state changes
*/
interface I18nReadInternalOptimizedProps extends II18nReadProps {
localeContext: ILocaleContextType;
mod: Module;
idef: ItemDefinition;
incl: Include;
}
/**
* The optimizer class just pipes to the internal
*/
export class I18nReadInternalOptimized extends React.PureComponent {
public render() {
if (!this.props.i18nId) {
return this.props.children ? this.props.children("MISSING ID") : "MISSING ID";
};
return i18nReadInternal(
this.props.localeContext,
this.props.mod,
this.props.idef,
this.props.incl,
this.props,
);
};
}
/**
* Allows to read localized properties from the properties
* file as they are available in the current context
*
* @param props the props
* @returns a react node
*/
export default function I18nRead(props: II18nReadProps) {
if (props.context) {
return (
{
(localeContext) => (
{
(root) => {
const rootV = root.root;
const itemDefOrModule = rootV.registry[props.context];
const idef = itemDefOrModule instanceof ItemDefinition ? itemDefOrModule : null;
const mod: Module = idef ? idef.getParentModule() : itemDefOrModule as Module;
if (!idef && !mod) {
throw new Error("The given context for " + props.context + " does not exist");
}
return (
);
}
}
)
}
)
}
return (
{
(localeContext) => (
{
(moduleContextualValue) => (
{
(itemContextualValue) => (
)
}
)
}
)
}
);
}
/**
* The i18n read as a react hook
* @param options
* @returns
*/
export function useI18nRead(options: I18nReadOptions): React.ReactNode {
const localeContext = useContext(LocaleContext);
const moduleContextualValue = useContext(ModuleContext);
const itemContextualValue = useContext(ItemContext);
const root = useRootRetriever();
if (!options.i18nId) {
return "MISSING I18n ID";
};
if (options.context) {
const rootV = root.root;
const itemDefOrModule = rootV.registry[options.context];
const idef = itemDefOrModule instanceof ItemDefinition ? itemDefOrModule : null;
const mod: Module = idef ? idef.getParentModule() : itemDefOrModule as Module;
if (!idef && !mod) {
throw new Error("The given context for " + options.context + " does not exist");
}
return i18nReadInternal(
localeContext,
mod,
idef,
null,
options,
);
}
return i18nReadInternal(
localeContext,
moduleContextualValue && moduleContextualValue.mod,
itemContextualValue && itemContextualValue.idef,
itemContextualValue && options.include && itemContextualValue.idef.getIncludeFor(options.include),
options,
);
}