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.
- package/.github/pull_request.yml +18 -0
- package/CONTRIBUTING.md +67 -0
- package/LICENSE +21 -0
- package/README.md +142 -0
- package/app/app.vue +106 -0
- package/app/assets/main.css +974 -0
- package/app/components/Api/EndpointCard.vue +169 -0
- package/app/components/Api/SchemaDocsTable.vue +208 -0
- package/app/components/Asset/Card.vue +1363 -0
- package/app/components/Asset/Grid.vue +295 -0
- package/app/components/Asset/Icon.vue +38 -0
- package/app/components/Asset/Preview.vue +486 -0
- package/app/components/Asset/Scanner.vue +700 -0
- package/app/components/Asset/Thumb.vue +708 -0
- package/app/components/Auth/Reset.vue +99 -0
- package/app/components/Auth/index.vue +346 -0
- package/app/components/ChatInterface.vue +2326 -0
- package/app/components/ChatPagePreview.vue +34 -0
- package/app/components/Column/Array.vue +29 -0
- package/app/components/Column/Asset.vue +21 -0
- package/app/components/Column/Boolean.vue +12 -0
- package/app/components/Column/Color.vue +9 -0
- package/app/components/Column/Date.vue +57 -0
- package/app/components/Column/Edit.vue +215 -0
- package/app/components/Column/Email.vue +9 -0
- package/app/components/Column/Html.vue +27 -0
- package/app/components/Column/Id.vue +20 -0
- package/app/components/Column/Locale.vue +32 -0
- package/app/components/Column/Object.vue +18 -0
- package/app/components/Column/Password.vue +7 -0
- package/app/components/Column/Role.vue +15 -0
- package/app/components/Column/S.vue +14 -0
- package/app/components/Column/Table/Single.vue +49 -0
- package/app/components/Column/Table/index.vue +42 -0
- package/app/components/Column/Tags.vue +31 -0
- package/app/components/Column/Text.vue +9 -0
- package/app/components/Column/Url.vue +9 -0
- package/app/components/Column/index.vue +41 -0
- package/app/components/Dashboard/Editor.vue +221 -0
- package/app/components/Dashboard/Grid.vue +270 -0
- package/app/components/Dashboard/View.vue +131 -0
- package/app/components/Dashboard/Widget/BarChart.vue +73 -0
- package/app/components/Dashboard/Widget/Counter.vue +51 -0
- package/app/components/Dashboard/Widget/LineChart.vue +76 -0
- package/app/components/Dashboard/Widget/PieChart.vue +75 -0
- package/app/components/Dashboard/Widget/Table.vue +94 -0
- package/app/components/Dashboard/WidgetEditor.vue +274 -0
- package/app/components/Data/Array.vue +102 -0
- package/app/components/Data/Html.vue +9 -0
- package/app/components/Data/Icon.vue +9 -0
- package/app/components/Data/Object.vue +10 -0
- package/app/components/Data/S.vue +16 -0
- package/app/components/Data/Table.vue +53 -0
- package/app/components/Data/Value.vue +34 -0
- package/app/components/Data/index.vue +14 -0
- package/app/components/Field/Array.vue +324 -0
- package/app/components/Field/Asset/Actions.vue +97 -0
- package/app/components/Field/Asset/index.vue +639 -0
- package/app/components/Field/Boolean.vue +26 -0
- package/app/components/Field/Checkbox.vue +48 -0
- package/app/components/Field/Color.vue +27 -0
- package/app/components/Field/Date.vue +30 -0
- package/app/components/Field/Email.vue +51 -0
- package/app/components/Field/EmailTemplate.vue +420 -0
- package/app/components/Field/Html/ColorPicker.vue +127 -0
- package/app/components/Field/Html/Editor.vue +544 -0
- package/app/components/Field/Html/index.vue +25 -0
- package/app/components/Field/Icon.vue +114 -0
- package/app/components/Field/JSON.vue +113 -0
- package/app/components/Field/Mention.vue +36 -0
- package/app/components/Field/Number.vue +27 -0
- package/app/components/Field/Object.vue +92 -0
- package/app/components/Field/Password.vue +55 -0
- package/app/components/Field/Radio.vue +47 -0
- package/app/components/Field/S.vue +44 -0
- package/app/components/Field/Select.vue +49 -0
- package/app/components/Field/Table.vue +348 -0
- package/app/components/Field/Tags.vue +113 -0
- package/app/components/Field/Text.vue +31 -0
- package/app/components/Field/Textarea.vue +33 -0
- package/app/components/Field/Url.vue +40 -0
- package/app/components/Field/Wrapper.vue +128 -0
- package/app/components/Field/index.vue +68 -0
- package/app/components/FloatingChatbot.vue +73 -0
- package/app/components/Form/Card.vue +151 -0
- package/app/components/Form/Drawer.vue +195 -0
- package/app/components/Form/index.vue +621 -0
- package/app/components/Header.vue +328 -0
- package/app/components/Offline/ConflictModal.vue +200 -0
- package/app/components/Offline/SyncStatus.vue +249 -0
- package/app/components/PlatformFeatureLanding.vue +65 -0
- package/app/components/Table/Backups.vue +638 -0
- package/app/components/Table/Flows.vue +814 -0
- package/app/components/Table/Grid.vue +296 -0
- package/app/components/Table/Icon.vue +36 -0
- package/app/components/Table/Logs.vue +181 -0
- package/app/components/Table/Schedules.vue +722 -0
- package/app/components/Table/Search/Button.vue +256 -0
- package/app/components/Table/Search/Items.vue +286 -0
- package/app/components/Table/Search/index.vue +87 -0
- package/app/components/Table/Settings/Schema.vue +801 -0
- package/app/components/Table/Settings/index.vue +631 -0
- package/app/components/Table/TranslateDrawer.vue +526 -0
- package/app/components/Table/Views/Kanban.vue +289 -0
- package/app/components/Table/Views/Table.vue +1273 -0
- package/app/components/Table/index.vue +890 -0
- package/app/composables/Translation/define.ts +41 -0
- package/app/composables/Translation/fetch.ts +107 -0
- package/app/composables/Translation/languages.ts +1 -0
- package/app/composables/Translation/loadCoreTranslations.ts +38 -0
- package/app/composables/Translation/translate.ts +250 -0
- package/app/composables/databaseCookies.ts +79 -0
- package/app/composables/fieldValidator.ts +72 -0
- package/app/composables/fieldsList.ts +301 -0
- package/app/composables/formatDatabase.ts +101 -0
- package/app/composables/index.ts +444 -0
- package/app/composables/openDrawer.ts +118 -0
- package/app/composables/optimizeFile.ts +287 -0
- package/app/composables/renderLabel.ts +88 -0
- package/app/composables/schemaClipboard.ts +95 -0
- package/app/composables/search.ts +353 -0
- package/app/composables/setThemeConfig.ts +48 -0
- package/app/composables/translationValue.ts +169 -0
- package/app/composables/useAssetPreview.ts +84 -0
- package/app/composables/useAssetUploader.ts +56 -0
- package/app/composables/useDashboardData.ts +310 -0
- package/app/composables/useDocumentThumbnail.ts +351 -0
- package/app/composables/useEcharts.ts +33 -0
- package/app/composables/useLocalDatabaseConfig.ts +51 -0
- package/app/composables/useOcr.ts +300 -0
- package/app/composables/useOfflineFetch.ts +194 -0
- package/app/composables/useOfflineItem.ts +107 -0
- package/app/composables/useOfflineQueue.ts +302 -0
- package/app/composables/useOfflineSync.ts +235 -0
- package/app/composables/usePasskeyAuth.ts +156 -0
- package/app/composables/usePdfCompressor.ts +180 -0
- package/app/composables/usePlatformRedirect.ts +72 -0
- package/app/composables/useRealtimeSync.ts +233 -0
- package/app/composables/useReferencedItems.ts +180 -0
- package/app/composables/useScanner.ts +664 -0
- package/app/composables/useSubscription.ts +650 -0
- package/app/composables/useThumbnailCache.ts +136 -0
- package/app/composables/useVideoCompressor.ts +194 -0
- package/app/error.vue +112 -0
- package/app/layouts/api.vue +188 -0
- package/app/layouts/dashboard.vue +91 -0
- package/app/layouts/default.vue +228 -0
- package/app/layouts/table.vue +471 -0
- package/app/locales/ar.ts +996 -0
- package/app/locales/en.ts +655 -0
- package/app/locales/es.ts +996 -0
- package/app/locales/fr.ts +997 -0
- package/app/middleware/dashboard.ts +33 -0
- package/app/middleware/database.ts +83 -0
- package/app/middleware/global.ts +1 -0
- package/app/middleware/sid-ingest.global.ts +29 -0
- package/app/middleware/table.ts +54 -0
- package/app/middleware/user.ts +26 -0
- package/app/pages/[[database]]/admin/api/auth/index.vue +306 -0
- package/app/pages/[[database]]/admin/api/index.vue +18 -0
- package/app/pages/[[database]]/admin/api/tables/[table]/index.vue +497 -0
- package/app/pages/[[database]]/admin/api/tables/index.vue +87 -0
- package/app/pages/[[database]]/admin/billing/index.vue +16 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/edit.vue +66 -0
- package/app/pages/[[database]]/admin/dashboards/[id]/index.vue +117 -0
- package/app/pages/[[database]]/admin/dashboards/index.vue +20 -0
- package/app/pages/[[database]]/admin/index.vue +28 -0
- package/app/pages/[[database]]/admin/settings.vue +526 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/edit.vue +143 -0
- package/app/pages/[[database]]/admin/tables/[table]/[id]/index.vue +146 -0
- package/app/pages/[[database]]/admin/tables/[table]/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/new.vue +12 -0
- package/app/pages/[[database]]/admin/tables/[table]/schedules.vue +10 -0
- package/app/pages/[[database]]/admin/tables/[table]/settings.vue +30 -0
- package/app/pages/[[database]]/admin/tables/assets/[...path].vue +24 -0
- package/app/pages/[[database]]/admin/tables/assets/flows.vue +10 -0
- package/app/pages/[[database]]/admin/tables/backups/index.vue +10 -0
- package/app/pages/[[database]]/admin/tables/blocks/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/index.vue +20 -0
- package/app/pages/[[database]]/admin/tables/pages/index.vue +16 -0
- package/app/pages/[[database]]/admin/tables/templates/index.vue +16 -0
- package/app/pages/[[database]]/api/index.vue +120 -0
- package/app/pages/[[database]]/auth/index.vue +12 -0
- package/app/pages/[[database]]/auth/reset.vue +10 -0
- package/app/pages/[[database]]/index.vue +10 -0
- package/app/plugins/offline-warmup.client.ts +53 -0
- package/app/plugins/template-globals.ts +9 -0
- package/app/spa-loading-template.html +93 -0
- package/biome.json +53 -0
- package/index.d.ts +380 -0
- package/modules/naiveui.ts +41 -0
- package/nuxt.config.ts +269 -0
- package/package.json +56 -0
- package/pnpm-workspace.yaml +5 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/pwa-192x192.png +0 -0
- package/public/pwa-512x512.png +0 -0
- package/public/pwa-maskable-192x192.png +0 -0
- package/public/pwa-maskable-512x512.png +0 -0
- package/public/vendor/opencv.js +48 -0
- 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
|
+
}
|