@userfrosting/sprinkle-core
Version:
Core Sprinkle for UserFrosting
294 lines (260 loc) • 11.4 kB
text/typescript
/**
* 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 }
)