@userfrosting/sprinkle-core 6.0.0-beta.7 → 6.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +27 -46
  2. package/dist/Page401Unauthorized-D_EoXicK.js +11 -0
  3. package/dist/Page403Forbidden-C96BgRtx.js +11 -0
  4. package/dist/Page404NotFound-CIs-2Fy2.js +11 -0
  5. package/dist/PageError-Sk-QAv9A.js +11 -0
  6. package/dist/_plugin-vue_export-helper-CHgC5LLL.js +9 -0
  7. package/dist/composables/index.d.ts +4 -0
  8. package/dist/composables/useAxiosInterceptor.d.ts +10 -0
  9. package/dist/composables/useCsrf.d.ts +18 -0
  10. package/dist/composables/useRuleSchemaAdapter.d.ts +7 -0
  11. package/dist/composables/useSprunjer.d.ts +2 -0
  12. package/dist/composables.js +151 -0
  13. package/dist/index.d.ts +18 -0
  14. package/dist/index.js +15 -0
  15. package/{app/assets/interfaces/ApiResponse.ts → dist/interfaces/ApiResponse.d.ts} +5 -6
  16. package/{app/assets/interfaces/DictionaryApi.ts → dist/interfaces/DictionaryApi.d.ts} +9 -11
  17. package/dist/interfaces/alerts.d.ts +8 -0
  18. package/{app/assets/interfaces/common.ts → dist/interfaces/common.d.ts} +1 -1
  19. package/dist/interfaces/index.d.ts +13 -0
  20. package/{app/assets/interfaces/severity.ts → dist/interfaces/severity.d.ts} +9 -9
  21. package/dist/interfaces/sprunjer.d.ts +51 -0
  22. package/{app/assets/interfaces/sprunjerApi.ts → dist/interfaces/sprunjerApi.d.ts} +12 -15
  23. package/dist/interfaces.js +5 -0
  24. package/dist/routes/index.d.ts +16 -0
  25. package/dist/routes.js +41 -0
  26. package/dist/severity-DwLpzIij.js +4 -0
  27. package/{app/assets/stores/Helpers/PluralRules.ts → dist/stores/Helpers/PluralRules.d.ts} +17 -114
  28. package/dist/stores/index.d.ts +4 -0
  29. package/dist/stores/useAlertsStore.d.ts +29 -0
  30. package/dist/stores/useConfigStore.d.ts +11 -0
  31. package/dist/stores/usePageMeta.d.ts +51 -0
  32. package/dist/stores/useTranslator.d.ts +66 -0
  33. package/dist/stores.js +7 -0
  34. package/dist/useAlertsStore-Ca6nXz8C.js +179 -0
  35. package/dist/useAxiosInterceptor-DcOpTLHG.js +68 -0
  36. package/dist/views/Page401Unauthorized.vue.d.ts +2 -0
  37. package/dist/views/Page403Forbidden.vue.d.ts +2 -0
  38. package/dist/views/Page404NotFound.vue.d.ts +2 -0
  39. package/dist/views/PageError.vue.d.ts +2 -0
  40. package/package.json +37 -9
  41. package/app/assets/composables/index.ts +0 -4
  42. package/app/assets/composables/useAxiosInterceptor.ts +0 -30
  43. package/app/assets/composables/useCsrf.ts +0 -129
  44. package/app/assets/composables/useRuleSchemaAdapter.ts +0 -205
  45. package/app/assets/composables/useSprunjer.ts +0 -188
  46. package/app/assets/index.d.ts +0 -8
  47. package/app/assets/index.ts +0 -40
  48. package/app/assets/interfaces/alerts.ts +0 -16
  49. package/app/assets/interfaces/index.ts +0 -30
  50. package/app/assets/interfaces/sprunjer.ts +0 -60
  51. package/app/assets/routes/index.ts +0 -44
  52. package/app/assets/stores/index.ts +0 -4
  53. package/app/assets/stores/useAlertsStore.ts +0 -30
  54. package/app/assets/stores/useConfigStore.ts +0 -30
  55. package/app/assets/stores/usePageMeta.ts +0 -114
  56. package/app/assets/stores/useTranslator.ts +0 -293
  57. package/app/assets/tests/composables/useCsrf.test.ts +0 -212
  58. package/app/assets/tests/composables/useRuleSchemaAdapter.test.ts +0 -657
  59. package/app/assets/tests/interfaces/alerts.test.ts +0 -43
  60. package/app/assets/tests/plugin.test.ts +0 -29
  61. package/app/assets/tests/stores/Helpers/PluralRules.test.ts +0 -440
  62. package/app/assets/tests/stores/config.test.ts +0 -42
  63. package/app/assets/tests/stores/useTranslator.test.ts +0 -373
  64. package/app/assets/views/Page401Unauthorized.vue +0 -3
  65. package/app/assets/views/Page403Forbidden.vue +0 -3
  66. package/app/assets/views/Page404NotFound.vue +0 -3
  67. package/app/assets/views/PageError.vue +0 -3
@@ -1,16 +0,0 @@
1
- /**
2
- * Alert Interface
3
- *
4
- * Represents a common interface for alert components. This interface is used by
5
- * API when an error occurs or a successful event occurs, and consumed by the
6
- * interface.
7
- */
8
- import { Severity } from './severity'
9
-
10
- export interface AlertInterface {
11
- title?: string
12
- description?: string
13
- style?: Severity | keyof typeof Severity
14
- closeBtn?: boolean
15
- hideIcon?: boolean
16
- }
@@ -1,30 +0,0 @@
1
- /**
2
- * Interface for custom routes meta fields.
3
- *
4
- * Meta Fields Added :
5
- * - title: string - Page title
6
- * - description: string - Page description
7
- *
8
- * Theses fields are used to set the document title and description, as well as
9
- * for breadcrumbs generation.
10
- *
11
- * @see https://router.vuejs.org/guide/advanced/meta.html#TypeScript
12
- */
13
- import 'vue-router'
14
-
15
- declare module 'vue-router' {
16
- interface RouteMeta {
17
- title?: string
18
- description?: string
19
- }
20
- }
21
-
22
- export type { AlertInterface } from './alerts'
23
- export type { AssociativeArray } from './common'
24
- export { Severity } from './severity'
25
- export type { Sprunjer, SprunjerData, SprunjerListable, SprunjerListableOption } from './sprunjer'
26
- export type { SprunjerRequest, SprunjerResponse } from './sprunjerApi'
27
- export type { DictionaryResponse, DictionaryEntries, DictionaryConfig } from './DictionaryApi'
28
-
29
- // Misc
30
- export type { ApiResponse, ApiErrorResponse } from './ApiResponse'
@@ -1,60 +0,0 @@
1
- /**
2
- * Sprunjer Interface
3
- *
4
- * Represents the interface for the Sprunjer composable.
5
- */
6
- import type { Ref, ComputedRef } from 'vue'
7
- import type { ApiErrorResponse, AssociativeArray } from '.'
8
-
9
- export interface Sprunjer {
10
- dataUrl: string | (() => string)
11
- size: Ref<number>
12
- page: Ref<number>
13
- sorts: Ref<AssociativeArray>
14
- filters: Ref<AssociativeArray>
15
- data: Ref<SprunjerData>
16
- fetch: () => void
17
- loading: Ref<boolean>
18
- error: Ref<ApiErrorResponse | null>
19
- totalPages: ComputedRef<number>
20
- downloadCsv: () => void
21
- countFiltered: ComputedRef<number>
22
- count: ComputedRef<number>
23
- rows: ComputedRef<any[]>
24
- first: ComputedRef<number>
25
- last: ComputedRef<number>
26
- toggleSort: (column: string) => void
27
- }
28
-
29
- /**
30
- * Sprunjer Data. Represents the data that is returned from any Sprunjer
31
- * Composable. It is different than SprunjerResponse, as the response if what
32
- * the API return, Data is what Vue provides. Both are similar, but Data doesn't
33
- * have optional values.
34
- *
35
- * N.B.: "rows" uses a generic array. It can contain any object, and should
36
- * actually be can be extended for each Sprunjer
37
- */
38
- export interface SprunjerData {
39
- count: number
40
- count_filtered: number
41
- rows: any[]
42
- listable: SprunjerListable
43
- sortable: string[]
44
- filterable: string[]
45
- }
46
-
47
- /**
48
- * Sprunjer Listable. Represents a listable for a Sprunjer.
49
- */
50
- export interface SprunjerListable {
51
- [key: string]: SprunjerListableOption[]
52
- }
53
-
54
- /**
55
- * Sprunjer Listable Option. Represents a listable option for a Sprunjer.
56
- */
57
- export interface SprunjerListableOption {
58
- value: string
59
- text: string
60
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Default error routes.
3
- *
4
- * N.B.: The first of these routes serve as a catch-all, so 404 not found must
5
- * be first.
6
- */
7
- export default [
8
- {
9
- path: '/:pathMatch(.*)*',
10
- name: 'NotFound',
11
- meta: {
12
- title: 'ERROR.404.TITLE',
13
- description: 'ERROR.404.DESCRIPTION'
14
- },
15
- component: () => import('../views/Page404NotFound.vue')
16
- },
17
- {
18
- path: '/:pathMatch(.*)*',
19
- name: 'Unauthorized',
20
- meta: {
21
- title: 'ERROR.401.TITLE',
22
- description: 'ERROR.401.DESCRIPTION'
23
- },
24
- component: () => import('../views/Page401Unauthorized.vue')
25
- },
26
- {
27
- path: '/:pathMatch(.*)*',
28
- name: 'Forbidden',
29
- meta: {
30
- title: 'ERROR.403.TITLE',
31
- description: 'ERROR.403.DESCRIPTION'
32
- },
33
- component: () => import('../views/Page403Forbidden.vue')
34
- },
35
- {
36
- path: '/:pathMatch(.*)*',
37
- name: 'Error',
38
- meta: {
39
- title: 'ERROR.TITLE',
40
- description: 'ERROR.DESCRIPTION'
41
- },
42
- component: () => import('../views/PageError.vue')
43
- }
44
- ]
@@ -1,4 +0,0 @@
1
- export { useConfigStore } from './useConfigStore'
2
- export { usePageMeta } from './usePageMeta'
3
- export { useTranslator } from './useTranslator'
4
- export { useAlertsStore } from './useAlertsStore'
@@ -1,30 +0,0 @@
1
- import { defineStore } from 'pinia'
2
- import type { AlertInterface } from '../interfaces'
3
-
4
- /**
5
- * Alerts Store
6
- *
7
- * Manages application alerts in a reactive store. Templates can use this
8
- * store to display alerts to users, including errors, warnings,
9
- * informational, or success messages. When an alert is added, templates
10
- * should automatically update the interface to reflect the change.
11
- */
12
- export const useAlertsStore = defineStore('alerts', {
13
- state: () => ({
14
- alerts: [] as AlertInterface[]
15
- }),
16
- actions: {
17
- push(alert: AlertInterface) {
18
- this.alerts.push(alert)
19
- },
20
- pop() {
21
- return this.alerts.pop()
22
- },
23
- shift() {
24
- return this.alerts.shift()
25
- },
26
- clear() {
27
- this.alerts = []
28
- }
29
- }
30
- })
@@ -1,30 +0,0 @@
1
- /**
2
- * Config Store
3
- *
4
- * This store is used to access the configuration of the application from the
5
- * API.
6
- */
7
- import { defineStore } from 'pinia'
8
- import axios from 'axios'
9
- import { getProperty } from 'dot-prop'
10
-
11
- export const useConfigStore = defineStore('config', {
12
- persist: true,
13
- state: () => {
14
- return {
15
- config: {}
16
- }
17
- },
18
- getters: {
19
- get: (state) => {
20
- return (key: string, value?: any): any => getProperty(state.config, key, value)
21
- }
22
- },
23
- actions: {
24
- async load() {
25
- axios.get('/api/config').then((response) => {
26
- this.config = response.data
27
- })
28
- }
29
- }
30
- })
@@ -1,114 +0,0 @@
1
- import { computed, ref, watch } from 'vue'
2
- import { useRoute } from 'vue-router'
3
- import { useTranslator } from '@userfrosting/sprinkle-core/stores'
4
- import { useConfigStore } from '../stores'
5
- import { defineStore } from 'pinia'
6
-
7
- /**
8
- * Page Meta Composable
9
- *
10
- * Handles the page meta data such as title, description, plus generate
11
- * breadcrumbs from the frontend router. The title, description and breadcrumbs
12
- * are updated automatically when the route changes.
13
- *
14
- * Available States : breadcrumbs, title, description
15
- */
16
- export const usePageMeta = defineStore('pageMeta', () => {
17
- /**
18
- * Globally provided properties
19
- */
20
- const route = useRoute()
21
- const { translate } = useTranslator()
22
-
23
- /**
24
- * States
25
- *
26
- * - title: The current page title
27
- * - description: The current page description
28
- * - breadcrumbs: The current page breadcrumbs
29
- * - hideBreadcrumbs: Ask the component to hide the breadcrumbs on the page
30
- * - hideTitle: Ask the component to hide the title on the page
31
- */
32
- const title = ref<string>('')
33
- const description = ref<string>('')
34
- const breadcrumbs = ref<Breadcrumb[]>([])
35
- const hideBreadcrumbs = ref<boolean>(false)
36
- const hideTitle = ref<boolean>(false)
37
-
38
- /**
39
- * Actions - Refresh the breadcrumbs, title and description
40
- */
41
- function refresh() {
42
- // Reset default visibility attributes
43
- hideBreadcrumbs.value = false
44
- hideTitle.value = false
45
-
46
- // Get route trail
47
- const matchedRoutes = route.matched
48
-
49
- // Filter to remove routes without title and assign values as defined
50
- // in the breadcrumbs interface.
51
- const crumbs = matchedRoutes
52
- .filter(
53
- (routeItem) => routeItem.meta.title !== undefined && routeItem.meta.title !== ''
54
- )
55
- .map((routeItem) => {
56
- return {
57
- label: routeItem.meta?.title || '',
58
- to: routeItem.path
59
- }
60
- })
61
-
62
- // Add site title as first breadcrumb
63
- crumbs.unshift({
64
- label: siteTitle.value,
65
- to: '/'
66
- })
67
-
68
- // Replace ref with new values
69
- breadcrumbs.value = crumbs
70
-
71
- // Update Page Title & Description with current route
72
- title.value = route.meta.title || ''
73
- description.value = translate(route.meta.description || '')
74
- }
75
-
76
- // Update the document title
77
- function updatePageTitle() {
78
- document.title = pageFullTitle.value
79
- }
80
-
81
- // Update the document description in the HTML meta tag
82
- function updatePageDescription() {
83
- const descriptionElement = document.querySelector('head meta[name="description"]')
84
- descriptionElement?.setAttribute('content', description.value)
85
- }
86
-
87
- /**
88
- * Computed Properties - Getters
89
- *
90
- * - siteTitle: Return the site title from the config Store
91
- * - pageFullTitle: Return the full page title
92
- */
93
- const siteTitle = computed<string>(() => useConfigStore().get('site.title') || '')
94
- const pageFullTitle = computed<string>(() => {
95
- return title.value ? translate(title.value) + ' | ' + siteTitle.value : siteTitle.value
96
- })
97
-
98
- /**
99
- * Watchers - route, page title and description changes
100
- */
101
- watch(route, refresh, { immediate: true })
102
- watch(pageFullTitle, updatePageTitle, { immediate: true })
103
- watch(description, updatePageDescription, { immediate: true })
104
-
105
- /**
106
- * Returns the states and actions
107
- */
108
- return { breadcrumbs, title, description, hideBreadcrumbs, hideTitle }
109
- })
110
-
111
- interface Breadcrumb {
112
- label: string
113
- to: string
114
- }
@@ -1,293 +0,0 @@
1
- /**
2
- * Translator composable store.
3
- *
4
- * This pinia store is used to access the translator and to use the translator.
5
- */
6
- import { ref } from 'vue'
7
- import { defineStore } from 'pinia'
8
- import axios from 'axios'
9
- import type { DictionaryEntries, DictionaryResponse, DictionaryConfig } from '../interfaces'
10
- import { DateTime } from 'luxon'
11
- import type { PluralRules } from './Helpers/PluralRules'
12
- import {
13
- rule0,
14
- rule1,
15
- rule2,
16
- rule3,
17
- rule4,
18
- rule5,
19
- rule6,
20
- rule7,
21
- rule8,
22
- rule9,
23
- rule10,
24
- rule11,
25
- rule12,
26
- rule13,
27
- rule14,
28
- rule15
29
- } from './Helpers/PluralRules'
30
-
31
- // List all available plural rules
32
- const rules: PluralRules = {
33
- 0: rule0,
34
- 1: rule1,
35
- 2: rule2,
36
- 3: rule3,
37
- 4: rule4,
38
- 5: rule5,
39
- 6: rule6,
40
- 7: rule7,
41
- 8: rule8,
42
- 9: rule9,
43
- 10: rule10,
44
- 11: rule11,
45
- 12: rule12,
46
- 13: rule13,
47
- 14: rule14,
48
- 15: rule15
49
- }
50
-
51
- export const useTranslator = defineStore(
52
- 'translator',
53
- () => {
54
- /**
55
- * Variables
56
- */
57
- const defaultPluralKey = 'plural'
58
- const identifier = ref<string>('')
59
- const dictionary = ref<DictionaryEntries>({})
60
- const config = ref<DictionaryConfig>({
61
- name: '',
62
- regional: '',
63
- authors: [],
64
- plural_rule: 0,
65
- dates: ''
66
- })
67
-
68
- /**
69
- * Functions
70
- */
71
- // Load the dictionary from the API
72
- async function load() {
73
- axios.get<DictionaryResponse>('/api/dictionary').then((response) => {
74
- identifier.value = response.data.identifier
75
- config.value = response.data.config
76
- dictionary.value = response.data.dictionary
77
- })
78
- }
79
-
80
- // The translate function
81
- function translate(key: string, placeholders: string | number | object = {}): string {
82
- const { message, placeholders: mutatedPlaceholders } = getMessageFromKey(
83
- key,
84
- placeholders
85
- )
86
- placeholders = mutatedPlaceholders
87
-
88
- return replacePlaceholders(message, placeholders)
89
- }
90
-
91
- /**
92
- * Format a date to the user locale
93
- *
94
- * @param date The date to format, in ISO format
95
- * @param format The format to use. Default to `DATETIME_MED_WITH_WEEKDAY`.
96
- * See the Luxon documentation for more information on formatting
97
- *
98
- * @see https://moment.github.io/luxon/#/formatting?id=presets
99
- * @see https://moment.github.io/luxon/#/formatting?id=table-of-tokens
100
- */
101
- function translateDate(
102
- date: string,
103
- format: string | object = DateTime.DATETIME_MED_WITH_WEEKDAY
104
- ): string {
105
- const dt = getDateTime(date)
106
- if (typeof format === 'object') {
107
- return dt.toLocaleString(format)
108
- } else {
109
- return dt.toFormat(format)
110
- }
111
- }
112
-
113
- /**
114
- * Returns the Luxon DateTime object for the given date, with the user
115
- * locale, so Luxon methods can be used without having to set the locale
116
- *
117
- * @param date The date to format, in ISO format
118
- */
119
- function getDateTime(date: string): DateTime {
120
- return DateTime.fromISO(date).setLocale(config.value.dates)
121
- }
122
-
123
- // TODO : Add doc + make Placeholders a type
124
- function getMessageFromKey(
125
- key: string,
126
- placeholders: string | number | Record<string, any>
127
- ): { message: string; placeholders: string | number | Record<string, any> } {
128
- // Return direct match
129
- if (dictionary.value[key] !== undefined) {
130
- return { message: dictionary.value[key], placeholders }
131
- }
132
-
133
- // First, let's see if we can get the plural rules.
134
- // A plural form will always have priority over the `@TRANSLATION` instruction
135
- // We start by picking up the plural key, aka which placeholder contains the numeric value defining how many {x} we have
136
- const pluralKey = dictionary.value[key + '.@PLURAL'] || defaultPluralKey
137
-
138
- // Let's get the plural value, aka how many {x} we have
139
- // If no plural value was found, we fallback to `@TRANSLATION` instruction or default to 1 as a last resort
140
- let pluralValue: number = 1
141
- if (typeof placeholders === 'object' && placeholders[pluralKey] !== undefined) {
142
- pluralValue = Number(placeholders[pluralKey])
143
- } else if (typeof placeholders === 'number' || typeof placeholders === 'string') {
144
- pluralValue = Number(placeholders)
145
- } else if (dictionary.value[key + '.@TRANSLATION'] !== undefined) {
146
- // We have a `@TRANSLATION` instruction, return this
147
- return { message: dictionary.value[key + '.@TRANSLATION'], placeholders }
148
- }
149
-
150
- // If placeholders is a numeric value, we transform back to an array for replacement in the main message
151
- if (typeof placeholders === 'number' || typeof placeholders === 'string') {
152
- placeholders = { [pluralKey]: pluralValue }
153
- } else if (typeof placeholders === 'object' && placeholders[pluralKey] === undefined) {
154
- placeholders = { ...placeholders, [pluralKey]: pluralValue }
155
- }
156
-
157
- // At this point, we need to go deeper and find the correct plural form to use
158
- const pluralRuleKey = getPluralMessageKey(key, pluralValue)
159
-
160
- // Only return if the plural is not null. Will happen if the message array don't follow the rules
161
- if (dictionary.value[key + '.' + pluralRuleKey] !== undefined) {
162
- return { message: dictionary.value[key + '.' + pluralRuleKey], placeholders }
163
- }
164
-
165
- // One last check... If we don't have a rule, but the $pluralValue
166
- // as a key does exist, we might still be able to return it
167
- if (dictionary.value[key + '.' + pluralValue] !== undefined) {
168
- return { message: dictionary.value[key + '.' + pluralValue], placeholders }
169
- }
170
-
171
- // Return @TRANSLATION match
172
- if (dictionary.value[key + '.@TRANSLATION'] !== undefined) {
173
- return { message: dictionary.value[key + '.@TRANSLATION'], placeholders }
174
- }
175
-
176
- // If the message is an array, but we can't find a plural form or a "@TRANSLATION" instruction, we can't go further.
177
- // We can't return the array, so we'll return the key
178
- return { message: key, placeholders }
179
- }
180
-
181
- function replacePlaceholders(
182
- message: string,
183
- placeholders: string | number | Record<string, any>
184
- ): string {
185
- // If placeholders is not an object at this point, we make it an object, using `plural` as the key
186
- if (typeof placeholders !== 'object') {
187
- placeholders = { [defaultPluralKey]: placeholders }
188
- }
189
-
190
- // Interpolate translatable placeholders values. This allows to
191
- // pre-translate placeholder which value starts with the `&` character
192
- // console.debug('Looping Placeholders', placeholders)
193
- for (const [name, value] of Object.entries(placeholders)) {
194
- // console.debug(`> ${name}: ${value}`)
195
-
196
- //We don't allow nested placeholders. They will return errors on the next lines
197
- if (typeof value !== 'string') {
198
- continue
199
- }
200
-
201
- // We test if the placeholder value starts the "&" character.
202
- // That means we need to translate that placeholder value
203
- if (value.startsWith('&')) {
204
- // Remove the current placeholder from the master $placeholder
205
- // array, otherwise we end up in an infinite loop
206
- const data = Object.fromEntries(
207
- Object.entries(placeholders).filter(([k]) => k !== name)
208
- )
209
-
210
- // Translate placeholders value and place it in the main $placeholder array
211
- placeholders[name] = translate(value.substring(1), data)
212
- }
213
- }
214
-
215
- // We check for {{&...}} strings in the resulting message.
216
- // While the previous loop pre-translated placeholder value, this one
217
- // pre-translate the message string vars
218
- // We use some regex magic to detect them !
219
- message = message.replace(/{{&(([^}]+[^a-z]))}}/g, (match, p1) => {
220
- return translate(p1, placeholders)
221
- })
222
-
223
- // Now it's time to replace the remaining placeholder.
224
- for (const [name, value] of Object.entries(placeholders)) {
225
- const regex = new RegExp(`{{${name}}}`, 'g')
226
- message = message.replace(regex, String(value))
227
- }
228
-
229
- return message
230
- }
231
-
232
- /**
233
- * Return the correct plural message form to use.
234
- * When multiple plural form are available for a message, this method will return the correct oen to use based on the numeric value.
235
- *
236
- * @param int $pluralValue The numeric value used to select the correct message
237
- *
238
- * @return int|null Returns which key from $messageArray to use
239
- */
240
- function getPluralMessageKey(key: string, pluralValue: number): number | null {
241
- // Bypass the rules for a value of "0". Instead of returning the
242
- // correct plural form (>= 1), we force return the "0" form, which
243
- // can used to display "0 users" as "No users".
244
- if (pluralValue === 0 && dictionary.value[key + '.0'] !== undefined) {
245
- return 0
246
- }
247
-
248
- // Get the correct plural form to use depending on the language
249
- const pluralForm = getPluralForm(pluralValue)
250
-
251
- // If the dictionary contains a string for this form, return the form
252
- if (dictionary.value[key + '.' + pluralForm] !== undefined) {
253
- return pluralForm
254
- }
255
-
256
- // If the key we need doesn't exist, use the previous available one, including the special "0" form
257
- // This is a fallback to avoid errors when the dictionary is not complete
258
- for (let i = pluralForm; i >= 0; i--) {
259
- if (dictionary.value[key + '.' + i] !== undefined) {
260
- return i
261
- }
262
- }
263
-
264
- // If no key was found, null will be returned
265
- return null
266
- }
267
-
268
- function getPluralForm(pluralValue: number, forceRule?: number): number {
269
- const rule = forceRule ?? config.value.plural_rule
270
-
271
- if (rule < 0 || rule >= Object.keys(rules).length) {
272
- throw new Error(`The rule number ${rule} must be between 0 and 15`)
273
- }
274
-
275
- return rules[rule](pluralValue)
276
- }
277
-
278
- /**
279
- * Return store
280
- */
281
- return {
282
- dictionary,
283
- load,
284
- config,
285
- identifier,
286
- translate,
287
- translateDate,
288
- getPluralForm,
289
- getDateTime
290
- }
291
- },
292
- { persist: true }
293
- )