UNPKG

@userfrosting/sprinkle-core

Version:
294 lines (260 loc) 11.4 kB
/** * Translator composable store. * * This pinia store is used to access the translator and to use the translator. */ import { ref } from 'vue' import { defineStore } from 'pinia' import axios from 'axios' import type { DictionaryEntries, DictionaryResponse, DictionaryConfig } from '../interfaces' import { DateTime } from 'luxon' import type { PluralRules } from './Helpers/PluralRules' import { rule0, rule1, rule2, rule3, rule4, rule5, rule6, rule7, rule8, rule9, rule10, rule11, rule12, rule13, rule14, rule15 } from './Helpers/PluralRules' // List all available plural rules const rules: PluralRules = { 0: rule0, 1: rule1, 2: rule2, 3: rule3, 4: rule4, 5: rule5, 6: rule6, 7: rule7, 8: rule8, 9: rule9, 10: rule10, 11: rule11, 12: rule12, 13: rule13, 14: rule14, 15: rule15 } export const useTranslator = defineStore( 'translator', () => { /** * Variables */ const defaultPluralKey = 'plural' const identifier = ref<string>('') const dictionary = ref<DictionaryEntries>({}) const config = ref<DictionaryConfig>({ name: '', regional: '', authors: [], plural_rule: 0, dates: '' }) /** * Functions */ // Load the dictionary from the API async function load() { axios.get<DictionaryResponse>('/api/dictionary').then((response) => { identifier.value = response.data.identifier config.value = response.data.config dictionary.value = response.data.dictionary }) } // The translate function function translate(key: string, placeholders: string | number | object = {}): string { const { message, placeholders: mutatedPlaceholders } = getMessageFromKey( key, placeholders ) placeholders = mutatedPlaceholders return replacePlaceholders(message, placeholders) } /** * Format a date to the user locale * * @param date The date to format, in ISO format * @param format The format to use. Default to `DATETIME_MED_WITH_WEEKDAY`. * See the Luxon documentation for more information on formatting * * @see https://moment.github.io/luxon/#/formatting?id=presets * @see https://moment.github.io/luxon/#/formatting?id=table-of-tokens */ function translateDate( date: string, format: string | object = DateTime.DATETIME_MED_WITH_WEEKDAY ): string { const dt = getDateTime(date) if (typeof format === 'object') { return dt.toLocaleString(format) } else { return dt.toFormat(format) } } /** * Returns the Luxon DateTime object for the given date, with the user * locale, so Luxon methods can be used without having to set the locale * * @param date The date to format, in ISO format */ function getDateTime(date: string): DateTime { return DateTime.fromISO(date).setLocale(config.value.dates) } // TODO : Add doc + make Placeholders a type function getMessageFromKey( key: string, placeholders: string | number | Record<string, any> ): { message: string; placeholders: string | number | Record<string, any> } { // Return direct match if (dictionary.value[key] !== undefined) { return { message: dictionary.value[key], placeholders } } // First, let's see if we can get the plural rules. // A plural form will always have priority over the `@TRANSLATION` instruction // We start by picking up the plural key, aka which placeholder contains the numeric value defining how many {x} we have const pluralKey = dictionary.value[key + '.@PLURAL'] || defaultPluralKey // Let's get the plural value, aka how many {x} we have // If no plural value was found, we fallback to `@TRANSLATION` instruction or default to 1 as a last resort let pluralValue: number = 1 if (typeof placeholders === 'object' && placeholders[pluralKey] !== undefined) { pluralValue = Number(placeholders[pluralKey]) } else if (typeof placeholders === 'number' || typeof placeholders === 'string') { pluralValue = Number(placeholders) } else if (dictionary.value[key + '.@TRANSLATION'] !== undefined) { // We have a `@TRANSLATION` instruction, return this return { message: dictionary.value[key + '.@TRANSLATION'], placeholders } } // If placeholders is a numeric value, we transform back to an array for replacement in the main message if (typeof placeholders === 'number' || typeof placeholders === 'string') { placeholders = { [pluralKey]: pluralValue } } else if (typeof placeholders === 'object' && placeholders[pluralKey] === undefined) { placeholders = { ...placeholders, [pluralKey]: pluralValue } } // At this point, we need to go deeper and find the correct plural form to use const pluralRuleKey = getPluralMessageKey(key, pluralValue) // Only return if the plural is not null. Will happen if the message array don't follow the rules if (dictionary.value[key + '.' + pluralRuleKey] !== undefined) { return { message: dictionary.value[key + '.' + pluralRuleKey], placeholders } } // One last check... If we don't have a rule, but the $pluralValue // as a key does exist, we might still be able to return it if (dictionary.value[key + '.' + pluralValue] !== undefined) { return { message: dictionary.value[key + '.' + pluralValue], placeholders } } // Return @TRANSLATION match if (dictionary.value[key + '.@TRANSLATION'] !== undefined) { return { message: dictionary.value[key + '.@TRANSLATION'], placeholders } } // If the message is an array, but we can't find a plural form or a "@TRANSLATION" instruction, we can't go further. // We can't return the array, so we'll return the key return { message: key, placeholders } } function replacePlaceholders( message: string, placeholders: string | number | Record<string, any> ): string { // If placeholders is not an object at this point, we make it an object, using `plural` as the key if (typeof placeholders !== 'object') { placeholders = { [defaultPluralKey]: placeholders } } // Interpolate translatable placeholders values. This allows to // pre-translate placeholder which value starts with the `&` character // console.debug('Looping Placeholders', placeholders) for (const [name, value] of Object.entries(placeholders)) { // console.debug(`> ${name}: ${value}`) //We don't allow nested placeholders. They will return errors on the next lines if (typeof value !== 'string') { continue } // We test if the placeholder value starts the "&" character. // That means we need to translate that placeholder value if (value.startsWith('&')) { // Remove the current placeholder from the master $placeholder // array, otherwise we end up in an infinite loop const data = Object.fromEntries( Object.entries(placeholders).filter(([k]) => k !== name) ) // Translate placeholders value and place it in the main $placeholder array placeholders[name] = translate(value.substring(1), data) } } // We check for {{&...}} strings in the resulting message. // While the previous loop pre-translated placeholder value, this one // pre-translate the message string vars // We use some regex magic to detect them ! message = message.replace(/{{&(([^}]+[^a-z]))}}/g, (match, p1) => { return translate(p1, placeholders) }) // Now it's time to replace the remaining placeholder. for (const [name, value] of Object.entries(placeholders)) { const regex = new RegExp(`{{${name}}}`, 'g') message = message.replace(regex, String(value)) } return message } /** * Return the correct plural message form to use. * When multiple plural form are available for a message, this method will return the correct oen to use based on the numeric value. * * @param int $pluralValue The numeric value used to select the correct message * * @return int|null Returns which key from $messageArray to use */ function getPluralMessageKey(key: string, pluralValue: number): number | null { // Bypass the rules for a value of "0". Instead of returning the // correct plural form (>= 1), we force return the "0" form, which // can used to display "0 users" as "No users". if (pluralValue === 0 && dictionary.value[key + '.0'] !== undefined) { return 0 } // Get the correct plural form to use depending on the language const pluralForm = getPluralForm(pluralValue) // If the dictionary contains a string for this form, return the form if (dictionary.value[key + '.' + pluralForm] !== undefined) { return pluralForm } // If the key we need doesn't exist, use the previous available one, including the special "0" form // This is a fallback to avoid errors when the dictionary is not complete for (let i = pluralForm; i >= 0; i--) { if (dictionary.value[key + '.' + i] !== undefined) { return i } } // If no key was found, null will be returned return null } function getPluralForm(pluralValue: number, forceRule?: number): number { const rule = forceRule ?? config.value.plural_rule if (rule < 0 || rule >= Object.keys(rules).length) { throw new Error(`The rule number ${rule} must be between 0 and 15`) } return rules[rule](pluralValue) } /** * Return store */ return { dictionary, load, config, identifier, translate, translateDate, getPluralForm, getDateTime } }, { persist: true } )