/* This file is part of GNU Taler (C) 2021 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ /** * Translation helpers for React components and template literals. */ /** * Imports */ import { ComponentChild, ComponentChildren, h, Fragment, VNode } from 'preact'; import { useTranslationContext } from '../context/translation'; export function useTranslator() { const ctx = useTranslationContext(); const jed = ctx.handler; return function str( stringSeq: TemplateStringsArray, ...values: any[] ): string { const s = toI18nString(stringSeq); if (!s) return s; const tr = jed .translate(s) .ifPlural(1, s) .fetch(...values); return tr; }; } /** * Convert template strings to a msgid */ function toI18nString(stringSeq: ReadonlyArray): string { let s = ''; for (let i = 0; i < stringSeq.length; i++) { s += stringSeq[i]; if (i < stringSeq.length - 1) s += `%${i + 1}$s`; } return s; } interface TranslateSwitchProps { target: number; children: ComponentChildren; } function stringifyChildren(children: ComponentChildren): string { let n = 1; const ss = (children instanceof Array ? children : [children]).map((c) => { if (typeof c === 'string') return c; return `%${n++}$s`; }); const s = ss.join('').replace(/ +/g, ' ').trim(); return s; } interface TranslateProps { children: ComponentChildren; /** * Component that the translated element should be wrapped in. * Defaults to "div". */ wrap?: any; /** * Props to give to the wrapped component. */ wrapProps?: any; } function getTranslatedChildren( translation: string, children: ComponentChildren, ): ComponentChild[] { const tr = translation.split(/%(\d+)\$s/); const childArray = children instanceof Array ? children : [children]; // Merge consecutive string children. const placeholderChildren = Array(); for (let i = 0; i < childArray.length; i++) { const x = childArray[i]; if (x === undefined) continue; else if (typeof x === 'string') continue; else placeholderChildren.push(x); } const result = Array(); for (let i = 0; i < tr.length; i++) if (i % 2 == 0) // Text result.push(tr[i]); else { const childIdx = Number.parseInt(tr[i], 10) - 1; result.push(placeholderChildren[childIdx]); } return result; } /** * Translate text node children of this component. * If a child component might produce a text node, it must be wrapped * in a another non-text element. * * Example: * ``` * * Hello. Your score is * * ``` */ export function Translate({ children }: TranslateProps): VNode { const s = stringifyChildren(children); const ctx = useTranslationContext(); const translation: string = ctx.handler.ngettext(s, s, 1); const result = getTranslatedChildren(translation, children); return {result}; } /** * Switch translation based on singular or plural based on the target prop. * Should only contain TranslateSingular and TransplatePlural as children. * * Example: * ``` * * I have {n} apple. * I have {n} apples. * * ``` */ export function TranslateSwitch({ children, target }: TranslateSwitchProps) { let singular: VNode | undefined; let plural: VNode | undefined; // const children = this.props.children; if (children) (children instanceof Array ? children : [children]).forEach( (child: any) => { if (child.type === TranslatePlural) plural = child; if (child.type === TranslateSingular) singular = child; }, ); if (!singular || !plural) { console.error('translation not found'); return h('span', {}, ['translation not found']); } singular.props.target = target; plural.props.target = target; // We're looking up the translation based on the // singular, even if we must use the plural form. return singular; } interface TranslationPluralProps { children: ComponentChildren; target: number; } /** * See [[TranslateSwitch]]. */ export function TranslatePlural({ children, target, }: TranslationPluralProps): VNode { const s = stringifyChildren(children); const ctx = useTranslationContext(); const translation = ctx.handler.ngettext(s, s, 1); const result = getTranslatedChildren(translation, children); return {result}; } /** * See [[TranslateSwitch]]. */ export function TranslateSingular({ children, target, }: TranslationPluralProps): VNode { const s = stringifyChildren(children); const ctx = useTranslationContext(); const translation = ctx.handler.ngettext(s, s, target); const result = getTranslatedChildren(translation, children); return {result}; }