inicontent 1.0.0

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 (205) hide show
  1. package/.github/pull_request.yml +18 -0
  2. package/CONTRIBUTING.md +67 -0
  3. package/LICENSE +21 -0
  4. package/README.md +142 -0
  5. package/app/app.vue +106 -0
  6. package/app/assets/main.css +974 -0
  7. package/app/components/Api/EndpointCard.vue +169 -0
  8. package/app/components/Api/SchemaDocsTable.vue +208 -0
  9. package/app/components/Asset/Card.vue +1363 -0
  10. package/app/components/Asset/Grid.vue +295 -0
  11. package/app/components/Asset/Icon.vue +38 -0
  12. package/app/components/Asset/Preview.vue +486 -0
  13. package/app/components/Asset/Scanner.vue +700 -0
  14. package/app/components/Asset/Thumb.vue +708 -0
  15. package/app/components/Auth/Reset.vue +99 -0
  16. package/app/components/Auth/index.vue +346 -0
  17. package/app/components/ChatInterface.vue +2326 -0
  18. package/app/components/ChatPagePreview.vue +34 -0
  19. package/app/components/Column/Array.vue +29 -0
  20. package/app/components/Column/Asset.vue +21 -0
  21. package/app/components/Column/Boolean.vue +12 -0
  22. package/app/components/Column/Color.vue +9 -0
  23. package/app/components/Column/Date.vue +57 -0
  24. package/app/components/Column/Edit.vue +215 -0
  25. package/app/components/Column/Email.vue +9 -0
  26. package/app/components/Column/Html.vue +27 -0
  27. package/app/components/Column/Id.vue +20 -0
  28. package/app/components/Column/Locale.vue +32 -0
  29. package/app/components/Column/Object.vue +18 -0
  30. package/app/components/Column/Password.vue +7 -0
  31. package/app/components/Column/Role.vue +15 -0
  32. package/app/components/Column/S.vue +14 -0
  33. package/app/components/Column/Table/Single.vue +49 -0
  34. package/app/components/Column/Table/index.vue +42 -0
  35. package/app/components/Column/Tags.vue +31 -0
  36. package/app/components/Column/Text.vue +9 -0
  37. package/app/components/Column/Url.vue +9 -0
  38. package/app/components/Column/index.vue +41 -0
  39. package/app/components/Dashboard/Editor.vue +221 -0
  40. package/app/components/Dashboard/Grid.vue +270 -0
  41. package/app/components/Dashboard/View.vue +131 -0
  42. package/app/components/Dashboard/Widget/BarChart.vue +73 -0
  43. package/app/components/Dashboard/Widget/Counter.vue +51 -0
  44. package/app/components/Dashboard/Widget/LineChart.vue +76 -0
  45. package/app/components/Dashboard/Widget/PieChart.vue +75 -0
  46. package/app/components/Dashboard/Widget/Table.vue +94 -0
  47. package/app/components/Dashboard/WidgetEditor.vue +274 -0
  48. package/app/components/Data/Array.vue +102 -0
  49. package/app/components/Data/Html.vue +9 -0
  50. package/app/components/Data/Icon.vue +9 -0
  51. package/app/components/Data/Object.vue +10 -0
  52. package/app/components/Data/S.vue +16 -0
  53. package/app/components/Data/Table.vue +53 -0
  54. package/app/components/Data/Value.vue +34 -0
  55. package/app/components/Data/index.vue +14 -0
  56. package/app/components/Field/Array.vue +324 -0
  57. package/app/components/Field/Asset/Actions.vue +97 -0
  58. package/app/components/Field/Asset/index.vue +639 -0
  59. package/app/components/Field/Boolean.vue +26 -0
  60. package/app/components/Field/Checkbox.vue +48 -0
  61. package/app/components/Field/Color.vue +27 -0
  62. package/app/components/Field/Date.vue +30 -0
  63. package/app/components/Field/Email.vue +51 -0
  64. package/app/components/Field/EmailTemplate.vue +420 -0
  65. package/app/components/Field/Html/ColorPicker.vue +127 -0
  66. package/app/components/Field/Html/Editor.vue +544 -0
  67. package/app/components/Field/Html/index.vue +25 -0
  68. package/app/components/Field/Icon.vue +114 -0
  69. package/app/components/Field/JSON.vue +113 -0
  70. package/app/components/Field/Mention.vue +36 -0
  71. package/app/components/Field/Number.vue +27 -0
  72. package/app/components/Field/Object.vue +92 -0
  73. package/app/components/Field/Password.vue +55 -0
  74. package/app/components/Field/Radio.vue +47 -0
  75. package/app/components/Field/S.vue +44 -0
  76. package/app/components/Field/Select.vue +49 -0
  77. package/app/components/Field/Table.vue +348 -0
  78. package/app/components/Field/Tags.vue +113 -0
  79. package/app/components/Field/Text.vue +31 -0
  80. package/app/components/Field/Textarea.vue +33 -0
  81. package/app/components/Field/Url.vue +40 -0
  82. package/app/components/Field/Wrapper.vue +128 -0
  83. package/app/components/Field/index.vue +68 -0
  84. package/app/components/FloatingChatbot.vue +73 -0
  85. package/app/components/Form/Card.vue +151 -0
  86. package/app/components/Form/Drawer.vue +195 -0
  87. package/app/components/Form/index.vue +621 -0
  88. package/app/components/Header.vue +328 -0
  89. package/app/components/Offline/ConflictModal.vue +200 -0
  90. package/app/components/Offline/SyncStatus.vue +249 -0
  91. package/app/components/PlatformFeatureLanding.vue +65 -0
  92. package/app/components/Table/Backups.vue +638 -0
  93. package/app/components/Table/Flows.vue +814 -0
  94. package/app/components/Table/Grid.vue +296 -0
  95. package/app/components/Table/Icon.vue +36 -0
  96. package/app/components/Table/Logs.vue +181 -0
  97. package/app/components/Table/Schedules.vue +722 -0
  98. package/app/components/Table/Search/Button.vue +256 -0
  99. package/app/components/Table/Search/Items.vue +286 -0
  100. package/app/components/Table/Search/index.vue +87 -0
  101. package/app/components/Table/Settings/Schema.vue +801 -0
  102. package/app/components/Table/Settings/index.vue +631 -0
  103. package/app/components/Table/TranslateDrawer.vue +526 -0
  104. package/app/components/Table/Views/Kanban.vue +289 -0
  105. package/app/components/Table/Views/Table.vue +1273 -0
  106. package/app/components/Table/index.vue +890 -0
  107. package/app/composables/Translation/define.ts +41 -0
  108. package/app/composables/Translation/fetch.ts +107 -0
  109. package/app/composables/Translation/languages.ts +1 -0
  110. package/app/composables/Translation/loadCoreTranslations.ts +38 -0
  111. package/app/composables/Translation/translate.ts +250 -0
  112. package/app/composables/databaseCookies.ts +79 -0
  113. package/app/composables/fieldValidator.ts +72 -0
  114. package/app/composables/fieldsList.ts +301 -0
  115. package/app/composables/formatDatabase.ts +101 -0
  116. package/app/composables/index.ts +444 -0
  117. package/app/composables/openDrawer.ts +118 -0
  118. package/app/composables/optimizeFile.ts +287 -0
  119. package/app/composables/renderLabel.ts +88 -0
  120. package/app/composables/schemaClipboard.ts +95 -0
  121. package/app/composables/search.ts +353 -0
  122. package/app/composables/setThemeConfig.ts +48 -0
  123. package/app/composables/translationValue.ts +169 -0
  124. package/app/composables/useAssetPreview.ts +84 -0
  125. package/app/composables/useAssetUploader.ts +56 -0
  126. package/app/composables/useDashboardData.ts +310 -0
  127. package/app/composables/useDocumentThumbnail.ts +351 -0
  128. package/app/composables/useEcharts.ts +33 -0
  129. package/app/composables/useLocalDatabaseConfig.ts +51 -0
  130. package/app/composables/useOcr.ts +300 -0
  131. package/app/composables/useOfflineFetch.ts +194 -0
  132. package/app/composables/useOfflineItem.ts +107 -0
  133. package/app/composables/useOfflineQueue.ts +302 -0
  134. package/app/composables/useOfflineSync.ts +235 -0
  135. package/app/composables/usePasskeyAuth.ts +156 -0
  136. package/app/composables/usePdfCompressor.ts +180 -0
  137. package/app/composables/usePlatformRedirect.ts +72 -0
  138. package/app/composables/useRealtimeSync.ts +233 -0
  139. package/app/composables/useReferencedItems.ts +180 -0
  140. package/app/composables/useScanner.ts +664 -0
  141. package/app/composables/useSubscription.ts +650 -0
  142. package/app/composables/useThumbnailCache.ts +136 -0
  143. package/app/composables/useVideoCompressor.ts +194 -0
  144. package/app/error.vue +112 -0
  145. package/app/layouts/api.vue +188 -0
  146. package/app/layouts/dashboard.vue +91 -0
  147. package/app/layouts/default.vue +228 -0
  148. package/app/layouts/table.vue +471 -0
  149. package/app/locales/ar.ts +996 -0
  150. package/app/locales/en.ts +655 -0
  151. package/app/locales/es.ts +996 -0
  152. package/app/locales/fr.ts +997 -0
  153. package/app/middleware/dashboard.ts +33 -0
  154. package/app/middleware/database.ts +83 -0
  155. package/app/middleware/global.ts +1 -0
  156. package/app/middleware/sid-ingest.global.ts +29 -0
  157. package/app/middleware/table.ts +54 -0
  158. package/app/middleware/user.ts +26 -0
  159. package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
  160. package/app/pages/[[database]]/admin/api/index.vue +18 -0
  161. package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
  162. package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
  163. package/app/pages/[[database]]/admin/billing/index.vue +16 -0
  164. package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
  165. package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
  166. package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
  167. package/app/pages/[[database]]/admin/index.vue +28 -0
  168. package/app/pages/[[database]]/admin/settings.vue +526 -0
  169. package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
  170. package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
  171. package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
  172. package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
  173. package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
  174. package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
  175. package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
  176. package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
  177. package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
  178. package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
  179. package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
  180. package/app/pages/[[database]]/admin/tables/index.vue +20 -0
  181. package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
  182. package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
  183. package/app/pages/[[database]]/api/index.vue +120 -0
  184. package/app/pages/[[database]]/auth/index.vue +12 -0
  185. package/app/pages/[[database]]/auth/reset.vue +10 -0
  186. package/app/pages/[[database]]/index.vue +10 -0
  187. package/app/plugins/offline-warmup.client.ts +53 -0
  188. package/app/plugins/template-globals.ts +9 -0
  189. package/app/spa-loading-template.html +93 -0
  190. package/biome.json +53 -0
  191. package/index.d.ts +380 -0
  192. package/modules/naiveui.ts +41 -0
  193. package/nuxt.config.ts +269 -0
  194. package/package.json +56 -0
  195. package/pnpm-workspace.yaml +5 -0
  196. package/public/apple-touch-icon.png +0 -0
  197. package/public/favicon-16x16.png +0 -0
  198. package/public/favicon-32x32.png +0 -0
  199. package/public/favicon.ico +0 -0
  200. package/public/pwa-192x192.png +0 -0
  201. package/public/pwa-512x512.png +0 -0
  202. package/public/pwa-maskable-192x192.png +0 -0
  203. package/public/pwa-maskable-512x512.png +0 -0
  204. package/public/vendor/opencv.js +48 -0
  205. package/tsconfig.json +9 -0
@@ -0,0 +1,41 @@
1
+ import { deepMerge } from "inibase/utils"
2
+
3
+ // Cache for translation hashes to avoid unnecessary merges
4
+ const translationCache = new Map<string, boolean>()
5
+
6
+ /**
7
+ * Generate a simple hash for a translations object
8
+ */
9
+ function hashTranslations(translations: TranslationsType): string {
10
+ return JSON.stringify(translations)
11
+ }
12
+
13
+ export default function (translations: TranslationsType) {
14
+ const translationsState = useState<TranslationsType>("translations")
15
+
16
+ // Generate hash of incoming translations
17
+ const hash = hashTranslations(translations)
18
+
19
+ // Check if we've already merged these exact translations
20
+ if (translationCache.has(hash)) {
21
+ return
22
+ }
23
+
24
+ // Mark this translation set as processed
25
+ translationCache.set(hash, true)
26
+
27
+ // Merge translations
28
+ if (translationsState.value) {
29
+ translationsState.value = deepMerge(translationsState.value, translations)
30
+ } else {
31
+ translationsState.value = translations
32
+ }
33
+
34
+ // Clean up cache if it gets too large (prevent memory leaks)
35
+ if (translationCache.size > 1000) {
36
+ const keysToDelete = Array.from(translationCache.keys()).slice(0, 500)
37
+ for (const key of keysToDelete) {
38
+ translationCache.delete(key)
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,107 @@
1
+ import Inison from "inison";
2
+
3
+ type TableTranslationOverlay = {
4
+ hadPrevious: boolean;
5
+ previous: unknown;
6
+ applied: string;
7
+ };
8
+
9
+ export default async function (onlyTableTranslations = false) {
10
+ const config = useRuntimeConfig();
11
+ const database = useState<Database>("database");
12
+ const Language = useScopedCookie<LanguagesType>("language", database.value?.slug);
13
+ const translationsState = useState<TranslationsType>("translations");
14
+ const tableOverlays = useState<
15
+ Record<string, Record<string, TableTranslationOverlay>>
16
+ >("tableTranslationOverlays", () => ({}));
17
+
18
+ const table = useState<Table>("table");
19
+ const sessionID = useScopedCookie<string>("sid", database.value?.slug);
20
+
21
+ if (!Language.value || !database.value) return;
22
+
23
+ const locale = Language.value;
24
+ const translationsByLocale = translationsState.value as unknown as Record<
25
+ string,
26
+ Record<string, string>
27
+ >;
28
+ const languageTranslations = () => translationsByLocale[locale] ?? {};
29
+
30
+ let fetchResult: Item[] = [];
31
+ let fetchSucceeded = false;
32
+ try {
33
+ const result = (
34
+ await $fetch<apiResponse<Item[]>>(
35
+ `${config.public.apiBase}${database.value.slug
36
+ }/translations`,
37
+ {
38
+ params: {
39
+ where: Inison.stringify({
40
+ locale,
41
+ original: "!=",
42
+ ...(table.value ? (onlyTableTranslations ? {table: table.value.id, field: 0} : {
43
+ table: { or: [table.value.id, null] },
44
+ field: 0,
45
+ }) : { table: null }),
46
+ }),
47
+ [`${database.value.slug}_sid`]: sessionID.value,
48
+ options: Inison.stringify({
49
+ perPage: 500,
50
+ columns: ["original", "translation", "table"],
51
+ }),
52
+ credentials: "include",
53
+ },
54
+ },
55
+ )
56
+ ).result;
57
+ fetchResult = result ?? [];
58
+ fetchSucceeded = true;
59
+ } catch (e) {
60
+ console.error("[Translation] fetch global translations error", e);
61
+ }
62
+
63
+ if (!translationsState.value) translationsState.value = {} as TranslationsType;
64
+ if (!translationsByLocale[locale]) translationsByLocale[locale] = {};
65
+ const currentTranslations = languageTranslations();
66
+
67
+ if (onlyTableTranslations && fetchSucceeded) {
68
+ const previousOverlays = tableOverlays.value[locale] ?? {};
69
+ for (const [key, overlay] of Object.entries(previousOverlays)) {
70
+ if (currentTranslations[key] !== overlay.applied) continue;
71
+ if (overlay.hadPrevious)
72
+ currentTranslations[key] = overlay.previous as string;
73
+ else delete currentTranslations[key];
74
+ }
75
+ delete tableOverlays.value[locale];
76
+ }
77
+
78
+ const merged: Record<string, string> = {};
79
+ for (const translation of fetchResult) {
80
+ if (
81
+ !onlyTableTranslations &&
82
+ (translation.table || translation.item || translation.field)
83
+ )
84
+ continue;
85
+ const original = translation.original;
86
+ const value = translation.translation;
87
+ if (!original || typeof value !== "string") continue;
88
+ merged[original] = value;
89
+ }
90
+
91
+ if (onlyTableTranslations) {
92
+ const overlays: Record<string, TableTranslationOverlay> = {};
93
+ for (const [key, value] of Object.entries(merged)) {
94
+ overlays[key] = {
95
+ hadPrevious: Object.prototype.hasOwnProperty.call(currentTranslations, key),
96
+ previous: currentTranslations[key],
97
+ applied: value,
98
+ };
99
+ }
100
+ tableOverlays.value[locale] = overlays;
101
+ }
102
+
103
+ translationsByLocale[locale] = {
104
+ ...currentTranslations,
105
+ ...merged,
106
+ };
107
+ }
@@ -0,0 +1 @@
1
+ export default ["ar", "en", "fr", "es"] as const
@@ -0,0 +1,38 @@
1
+ import defineTranslation from "./define"
2
+
3
+ // Track which languages have been loaded
4
+ const loadedLanguages = new Set<string>()
5
+
6
+ const locales = import.meta.glob("../../locales/*.ts")
7
+
8
+ /**
9
+ * Load centralized translation files
10
+ * This should be called once in app initialization
11
+ * @param lang - The initial language to load (default: 'en')
12
+ */
13
+ export default async function (
14
+ lang: LanguagesType = "en",
15
+ prevLang?: LanguagesType,
16
+ ) {
17
+ // Skip if already loaded
18
+ if (loadedLanguages.has(lang)) return
19
+
20
+ try {
21
+ const loader = locales[`../../locales/${lang}.ts`]
22
+ if (!loader) throw new Error(`Locale ${lang} not found`)
23
+
24
+ // Dynamically import the language file
25
+ const translations = (await loader()) as any
26
+
27
+ // Register the translations
28
+ defineTranslation(translations.default)
29
+
30
+ // Mark as loaded
31
+ loadedLanguages.add(lang)
32
+
33
+ if (prevLang && loadedLanguages.has(prevLang))
34
+ loadedLanguages.delete(prevLang)
35
+ } catch (error) {
36
+ console.error(`Failed to load ${lang} translations:`, error)
37
+ }
38
+ }
@@ -0,0 +1,250 @@
1
+ import { getProperty, hasProperty, setProperty } from "inidot";
2
+
3
+ function formatUnfoundTranslation(
4
+ input: string,
5
+ language: LanguagesType,
6
+ ): string {
7
+ input = decodeURI(input);
8
+ if (input.includes(".")) {
9
+ if (input.includes("..")) return input;
10
+ input = input.split(".").pop() ?? "";
11
+ }
12
+
13
+ const formatSupportedLanguages: LanguagesType[] = ["en", "es", "fr"];
14
+ if (
15
+ !formatSupportedLanguages.includes(language) ||
16
+ input.toUpperCase() === input
17
+ )
18
+ return input;
19
+
20
+ const lowercaseWordsMap: Record<LanguagesType, string[]> = {
21
+ ar: [],
22
+ en: [
23
+ "is",
24
+ "and",
25
+ "or",
26
+ "but",
27
+ "the",
28
+ "an",
29
+ "a",
30
+ "as",
31
+ "at",
32
+ "to",
33
+ "in",
34
+ "on",
35
+ ],
36
+ es: [
37
+ "y",
38
+ "o",
39
+ "pero",
40
+ "el",
41
+ "la",
42
+ "los",
43
+ "las",
44
+ "un",
45
+ "una",
46
+ "de",
47
+ "del",
48
+ "al",
49
+ "en",
50
+ "con",
51
+ "por",
52
+ "para",
53
+ "sin",
54
+ "sobre",
55
+ ],
56
+ fr: [
57
+ "et",
58
+ "ou",
59
+ "mais",
60
+ "le",
61
+ "la",
62
+ "les",
63
+ "un",
64
+ "une",
65
+ "des",
66
+ "du",
67
+ "de",
68
+ "dans",
69
+ "en",
70
+ "sur",
71
+ "avec",
72
+ "pour",
73
+ "sans",
74
+ "chez",
75
+ "par",
76
+ ],
77
+ };
78
+
79
+ const lowercaseWords = lowercaseWordsMap[language] ?? lowercaseWordsMap.en;
80
+
81
+ if (input.length === 2)
82
+ return lowercaseWords.includes(input.toLowerCase())
83
+ ? input.toLowerCase()
84
+ : input.toUpperCase();
85
+
86
+ // Split by capital letters and underscores
87
+ const words = input.split(/_(?![A-Z]+)|(?<=[a-z])(?=[A-Z])/);
88
+
89
+ // Process each word
90
+ const formattedWords = words.map((word, index) => {
91
+ if (word.charAt(0) === "_") word = word.slice(1);
92
+
93
+ if (word.toUpperCase() === word) return word.trim();
94
+
95
+ // Capitalize the first character
96
+ if (index === 0 || !lowercaseWords.includes(word.toLowerCase()))
97
+ return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
98
+
99
+ // Check if the word is in the linking or lowercase words and convert to lowercase
100
+ if (lowercaseWords.includes(word.toLowerCase())) return word.toLowerCase();
101
+
102
+ // Replace underscores with space
103
+ const wordWithoutUnderscore = word.replace("_", " ");
104
+
105
+ // Add space before each capitalized character
106
+ return wordWithoutUnderscore
107
+ .replace(/([A-Z])/g, " $1")
108
+ .trim()
109
+ .toLowerCase();
110
+ });
111
+
112
+ // Join the formatted words with spaces
113
+ return formattedWords.join(" ");
114
+ }
115
+
116
+ /**
117
+ * Interpolate variables into a translation string
118
+ * Supports both {{variable}} and {variable} syntax
119
+ */
120
+ function interpolate(text: string, params: Record<string, unknown>): string {
121
+ return text.replace(/\{\{?\s*(\w+)\s*\}?\}/g, (_, key) => {
122
+ return params[key] !== undefined ? String(params[key]) : `{${key}}`;
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Get plural form based on count and language rules
128
+ */
129
+ function getPluralForm(
130
+ count: number,
131
+ language: LanguagesType,
132
+ ): "zero" | "one" | "two" | "few" | "many" | "other" {
133
+ // English pluralization rules
134
+ if (language === "en") {
135
+ if (count === 0) return "zero";
136
+ if (count === 1) return "one";
137
+ return "other";
138
+ }
139
+
140
+ // Arabic pluralization rules
141
+ if (language === "ar") {
142
+ if (count === 0) return "zero";
143
+ if (count === 1) return "one";
144
+ if (count === 2) return "two";
145
+ const mod100 = count % 100;
146
+ if (mod100 >= 3 && mod100 <= 10) return "few";
147
+ if (mod100 >= 11 && mod100 <= 99) return "many";
148
+ return "other";
149
+ }
150
+
151
+ // French pluralization rules
152
+ if (language === "fr") {
153
+ if (count === 0 || count === 1) return "one";
154
+ return "other";
155
+ }
156
+
157
+ // Spanish pluralization rules
158
+ if (language === "es") {
159
+ if (count === 1) return "one";
160
+ return "other";
161
+ }
162
+
163
+ // Default
164
+ return count === 1 ? "one" : "other";
165
+ }
166
+
167
+ /**
168
+ * Handle plural translations
169
+ */
170
+ function handlePlural(
171
+ translation: unknown,
172
+ count: number,
173
+ language: LanguagesType,
174
+ params?: Record<string, unknown>,
175
+ ): string {
176
+ if (typeof translation === "string")
177
+ return interpolate(translation, { ...params, count });
178
+
179
+ const pluralForm = getPluralForm(count, language);
180
+ const translationRecord =
181
+ translation && typeof translation === "object"
182
+ ? (translation as Record<string, unknown>)
183
+ : {};
184
+
185
+ // Try to find the exact form, then fallback to more general forms
186
+ for (const form of [pluralForm, "other", "one"]) {
187
+ const value = translationRecord[form];
188
+ if (typeof value === "string") {
189
+ let result = value;
190
+ if (params) {
191
+ result = interpolate(result, { ...params, count });
192
+ }
193
+ return result;
194
+ }
195
+ }
196
+
197
+ return String(translation);
198
+ }
199
+
200
+ type TranslationParams = Record<string, unknown> & {
201
+ count?: number;
202
+ returnObjects?: boolean;
203
+ };
204
+
205
+ export default function (
206
+ key: string | number | null | undefined,
207
+ params?: TranslationParams,
208
+ ): string {
209
+ if (!key) return "";
210
+ if (typeof key !== "string") return String(key);
211
+
212
+ const translationsState = useState<TranslationsType>("translations");
213
+ const database = useState<Database>("database");
214
+ const Language = useScopedCookie<LanguagesType>("language", database.value?.slug);
215
+
216
+ if (!hasProperty(translationsState.value ?? {}, `${Language.value}.${key}`)) {
217
+ const unfoundTranslationsState = useState<TranslationsType>(
218
+ "unfoundTranslations",
219
+ );
220
+ if (!unfoundTranslationsState.value) unfoundTranslationsState.value = {};
221
+ setProperty(unfoundTranslationsState.value, key, null);
222
+ }
223
+
224
+ let translation =
225
+ getProperty(translationsState.value ?? {}, `${Language.value}.${key}`) ??
226
+ formatUnfoundTranslation(key, Language.value);
227
+
228
+ if (params?.returnObjects) return translation;
229
+
230
+ // Handle pluralization if count is provided
231
+ if (params?.count !== undefined) {
232
+ translation = handlePlural(
233
+ translation,
234
+ params.count,
235
+ Language.value,
236
+ params,
237
+ );
238
+ }
239
+ // Handle interpolation if params are provided
240
+ else if (params && typeof translation === "string") {
241
+ translation = interpolate(translation, params);
242
+ }
243
+
244
+ // If the value is still not a string (e.g. the key is a parent node with
245
+ // nested translations, like t("form")), return just the key instead of the
246
+ // object it resolved to.
247
+ if (typeof translation !== "string") return key;
248
+
249
+ return translation;
250
+ }
@@ -0,0 +1,79 @@
1
+ const syncedCookieKeys = ["language", "theme", "sid", "redirectTo"] as const;
2
+ // Only theme is shared globally across all databases. language, sid and
3
+ // redirectTo are scoped per database so each database keeps its own value.
4
+ const globalOnlyKeys = new Set(["theme"]);
5
+
6
+ function isSet(value: string | null | undefined) {
7
+ return value !== null && value !== undefined && value !== "";
8
+ }
9
+
10
+ function resolveDatabaseSlug(databaseSlug?: string) {
11
+ if (databaseSlug) return databaseSlug;
12
+
13
+ const config = useRuntimeConfig();
14
+ if (config.public.database && config.public.database !== "inicontent") {
15
+ return String(config.public.database);
16
+ }
17
+
18
+ const database = useState<Database | null>("database", () => null);
19
+ if (database.value?.slug) return database.value.slug;
20
+
21
+ const route = useRoute();
22
+ const routeDatabase = route.params.database;
23
+ if (Array.isArray(routeDatabase)) return routeDatabase[0] || "inicontent";
24
+ if (routeDatabase) return String(routeDatabase);
25
+
26
+ return "inicontent";
27
+ }
28
+
29
+ export function useScopedCookie<T>(key: string, databaseSlug?: string) {
30
+ return useCookie<T>(
31
+ `${resolveDatabaseSlug(databaseSlug)}_${key}`,
32
+ { sameSite: true },
33
+ );
34
+ }
35
+
36
+ export function syncCookiesFromDatabase(databaseSlug?: string) {
37
+ for (const key of syncedCookieKeys) {
38
+ const globalCookie = useCookie<string | null>(key, { sameSite: true });
39
+ const scopedCookie = useCookie<string | null>(
40
+ `${resolveDatabaseSlug(databaseSlug)}_${key}`,
41
+ {
42
+ sameSite: true,
43
+ },
44
+ );
45
+
46
+ if (isSet(scopedCookie.value)) {
47
+ // Always keep global in sync so components reading useCookie(key) work.
48
+ if (!globalOnlyKeys.has(key)) {
49
+ globalCookie.value = null;
50
+ } else {
51
+ globalCookie.value = scopedCookie.value;
52
+ }
53
+ } else if (isSet(globalCookie.value)) {
54
+ scopedCookie.value = globalCookie.value;
55
+ }
56
+ }
57
+ }
58
+
59
+ export function syncCookiesToDatabase(databaseSlug?: string) {
60
+ for (const key of syncedCookieKeys) {
61
+ const globalCookie = useCookie<string | null>(key, { sameSite: true });
62
+ const scopedCookie = useCookie<string | null>(
63
+ `${resolveDatabaseSlug(databaseSlug)}_${key}`,
64
+ {
65
+ sameSite: true,
66
+ },
67
+ );
68
+
69
+ if (isSet(globalCookie.value)) {
70
+ scopedCookie.value = globalCookie.value;
71
+ // For sid, wipe the global so the token is only in the scoped cookie.
72
+ if (!globalOnlyKeys.has(key)) {
73
+ globalCookie.value = null;
74
+ }
75
+ } else if (isSet(scopedCookie.value) && globalOnlyKeys.has(key)) {
76
+ globalCookie.value = scopedCookie.value;
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,72 @@
1
+ import { isArrayOfObjects } from "inibase/utils"
2
+ import Inison from "inison"
3
+
4
+ function applyRegex(field: Field, value?: string | (string | number)[]) {
5
+ if (field.regex && value) {
6
+ try {
7
+ const regex = new RegExp(field.regex)
8
+ const isValid = (() => {
9
+ let result = false
10
+ const timeout = setTimeout(() => {
11
+ throw new Error("Regex evaluation timeout")
12
+ }, 1000) // 1-second timeout
13
+ if (Array.isArray(value))
14
+ result = value.every((v) => regex.test(String(v)))
15
+ else result = regex.test(String(value))
16
+ clearTimeout(timeout)
17
+ return result
18
+ })()
19
+ if (!isValid) {
20
+ return new Error(`${t(field.key)} ${t("isInvalidFormat")}`)
21
+ }
22
+ } catch (error) {
23
+ return new Error(`${t(field.key)} ${t("isInvalidFormat")}`)
24
+ }
25
+ }
26
+ return true
27
+ }
28
+
29
+ export default function (
30
+ field: Field,
31
+ value: any,
32
+ extraValidator?: CallableFunction,
33
+ ): Promise<void> {
34
+ return new Promise<void>((resolve, reject) => {
35
+ if (field.isArray || Array.isArray(value)) {
36
+ if (!value || (Array.isArray(value) && value.length === 0))
37
+ if (field.required)
38
+ reject(new Error(`${t(field.key)} ${t("isRequired")}`))
39
+ else resolve()
40
+
41
+ if (Array.isArray(value) && field.min && value.length < field.min)
42
+ reject(new Error(`${t(field.key)} ${t("isNotValid")}`))
43
+
44
+ if (Array.isArray(value) && field.max && value.length > field.max)
45
+ reject(new Error(`${t(field.key)} ${t("isNotValid")}`))
46
+
47
+ const regex = applyRegex(
48
+ field,
49
+ isArrayOfObjects(value) ? Inison.stringify(value) : value,
50
+ )
51
+ if (regex instanceof Error) reject(regex)
52
+ } else {
53
+ if (
54
+ value === undefined ||
55
+ value === null ||
56
+ (typeof value === "string" && value.trim().length === 0)
57
+ )
58
+ if (field.required)
59
+ reject(new Error(`${t(field.key)} ${t("isRequired")}`))
60
+ else resolve()
61
+
62
+ const regex = applyRegex(field, value)
63
+ if (regex instanceof Error) reject(regex)
64
+ }
65
+
66
+ if (extraValidator)
67
+ if (!extraValidator(value, field))
68
+ reject(new Error(`${t(field.key)} ${t("isNotValid")}`))
69
+
70
+ resolve()
71
+ })
72
+ }