@brftech/filex-core 0.42.2 → 0.43.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/README.md +14 -2
- package/dist/ArchiveViewer-BqU761Wk.js +149 -0
- package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
- package/dist/CsvViewer-ByHVK5ga.js +172 -0
- package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
- package/dist/{DrawioViewer-CC6rJXeP.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-CAoTSXTO.js → EpubViewer-DJwUKlHW.js} +50 -50
- package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
- package/dist/MermaidViewer-CEDTD_pp.js +136 -0
- package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
- package/dist/PsdViewer-CZcxUpZO.js +121 -0
- package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
- package/dist/TiffViewer-BwI62CbQ.js +141 -0
- package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
- package/dist/{Viewer3D-CdZeK4j4.js → Viewer3D-DoeZcuMm.js} +16 -16
- package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
- package/dist/filex-core.js +475 -219
- package/dist/filex-core.umd.cjs +165 -108
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/fonts/caveat-latin-ext.woff2 +0 -0
- package/dist/fonts/caveat-latin.woff2 +0 -0
- package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/dist/fonts/dancing-script-latin.woff2 +0 -0
- package/dist/fonts/homemade-apple-latin.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/index-DuWnGuv5.js +35901 -0
- package/dist/index-DuWnGuv5.js.map +1 -0
- package/dist/index.d.ts +5227 -553
- package/dist/pdf-CaPYsOUo.js +21302 -0
- package/dist/pdf-CaPYsOUo.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +1055 -253
- package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
- package/src/assets/fonts/README.md +58 -0
- package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
- package/src/assets/fonts/caveat-latin.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
- package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
- package/src/components/AdvancedSearch.vue +49 -0
- package/src/components/Breadcrumb.vue +5 -5
- package/src/components/ChoiceButtons.vue +152 -0
- package/src/components/CommandPalette.vue +5 -5
- package/src/components/ConnectionGuideView.vue +1 -1
- package/src/components/ConnectionNotice.vue +59 -0
- package/src/components/ConnectionsPanel.vue +30 -568
- package/src/components/ContextMenu.vue +31 -5
- package/src/components/DataTable.vue +1331 -0
- package/src/components/FilePane.vue +3 -0
- package/src/components/FilterBar.vue +12 -5
- package/src/components/GalleryView.vue +50 -2
- package/src/components/GridView.vue +51 -3
- package/src/components/HomeView.vue +18 -6
- package/src/components/InspectorPanel.vue +134 -17
- package/src/components/ListView.vue +366 -900
- package/src/components/LogoMark.vue +57 -0
- package/src/components/NFSExportsPanel.vue +76 -28
- package/src/components/OnboardingTour.vue +19 -7
- package/src/components/OperationsCenter.vue +77 -1
- package/src/components/PendingOpsTray.vue +59 -5
- package/src/components/PresenceBar.vue +7 -7
- package/src/components/ProductVersion.vue +31 -0
- package/src/components/QuickLook.vue +10 -14
- package/src/components/RecentlyOpened.vue +31 -15
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -62
- package/src/components/SSHKeysPanel.vue +79 -30
- package/src/components/SideNav.vue +260 -96
- package/src/components/StarButton.vue +1 -0
- package/src/components/StorageFields.vue +74 -6
- package/src/components/StorageTags.vue +62 -0
- package/src/components/TabBar.vue +1 -1
- package/src/components/TagKindIcon.vue +56 -0
- package/src/components/TagPicker.vue +226 -40
- package/src/components/ThemePalette.vue +4 -4
- package/src/components/TimeZonePicker.vue +29 -10
- package/src/components/TokensPanel.vue +82 -31
- package/src/components/Toolbar.vue +28 -6
- package/src/components/plugin/PluginConfirmModal.vue +54 -0
- package/src/components/plugin/PluginInspectorSection.vue +149 -0
- package/src/components/plugin/PluginPageView.vue +339 -0
- package/src/components/plugin/PluginViewModal.vue +159 -0
- package/src/components/plugin/SurfaceConversation.vue +76 -0
- package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
- package/src/components/plugin/SurfaceRenderer.vue +299 -0
- package/src/components/plugin/SurfaceSections.vue +91 -0
- package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
- package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
- package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
- package/src/components/plugin/nodes/SurfaceList.vue +127 -0
- package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
- package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
- package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
- package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
- package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
- package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
- package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
- package/src/components/public/PublicLanguagePicker.vue +58 -0
- package/src/components/public/PublicLinkPage.vue +286 -0
- package/src/components/public/PublicLinkPreview.vue +85 -0
- package/src/components/public/PublicPinGate.vue +108 -0
- package/src/components/public/PublicRequestBody.vue +204 -0
- package/src/components/public/PublicShareBody.vue +188 -0
- package/src/components/public/PublicShell.vue +332 -0
- package/src/composables/useConnections.ts +8 -2
- package/src/composables/useFileApi.ts +183 -44
- package/src/composables/useLocale.ts +277 -41
- package/src/composables/useNFSExports.ts +8 -10
- package/src/composables/useOperations.ts +44 -3
- package/src/composables/usePendingOps.ts +59 -8
- package/src/composables/usePluginActions.ts +121 -0
- package/src/composables/usePluginSurface.ts +223 -0
- package/src/composables/usePublicBranding.ts +172 -0
- package/src/composables/usePublicLink.ts +573 -0
- package/src/composables/usePublicPage.ts +61 -0
- package/src/composables/useS3Keys.ts +17 -12
- package/src/composables/useSSHKeys.ts +8 -10
- package/src/composables/useTokens.ts +8 -12
- package/src/composables/useUploadChunked.ts +18 -8
- package/src/composables/useViewerFetch.ts +6 -3
- package/src/index.ts +404 -8
- package/src/lib/actionIcons.ts +46 -1
- package/src/lib/advSearch.ts +14 -7
- package/src/lib/appLock.ts +130 -0
- package/src/lib/connection.ts +110 -0
- package/src/lib/destinationTree.ts +19 -3
- package/src/lib/direction.ts +353 -0
- package/src/lib/dragGhost.ts +6 -0
- package/src/lib/dropLimits.ts +69 -0
- package/src/lib/errorWords.ts +394 -0
- package/src/lib/externalReach.ts +5 -1
- package/src/lib/fileFilters.ts +7 -2
- package/src/lib/fileIcons.ts +4 -0
- package/src/lib/internalPaths.ts +77 -0
- package/src/lib/listInput.ts +64 -0
- package/src/lib/listing.ts +58 -23
- package/src/lib/modalStack.ts +26 -0
- package/src/lib/nodeRow.ts +18 -3
- package/src/lib/pdfFieldRules.ts +126 -0
- package/src/lib/pdfFields.ts +589 -0
- package/src/lib/pdfFieldsGeom.ts +203 -0
- package/src/lib/pdfjsLoader.ts +51 -0
- package/src/lib/personName.ts +47 -0
- package/src/lib/pluginApplies.ts +123 -0
- package/src/lib/pluginLabel.ts +72 -0
- package/src/lib/pluginMenu.ts +253 -0
- package/src/lib/pluginPage.ts +58 -0
- package/src/lib/plural.ts +181 -0
- package/src/lib/popupLayer.ts +103 -0
- package/src/lib/prefs.ts +563 -0
- package/src/lib/productVersion.ts +35 -0
- package/src/lib/publicLayout.ts +39 -0
- package/src/lib/serviceGate.ts +104 -0
- package/src/lib/shareTtl.ts +25 -11
- package/src/lib/signFonts.ts +85 -0
- package/src/lib/signaturePad.ts +111 -0
- package/src/lib/sortOrder.ts +100 -38
- package/src/lib/star.ts +7 -1
- package/src/lib/surfaceCell.ts +41 -0
- package/src/lib/surfaceConditions.ts +156 -0
- package/src/lib/surfaceOpen.ts +106 -0
- package/src/lib/surfaceSteps.ts +68 -0
- package/src/lib/surfaceValues.ts +191 -0
- package/src/lib/symlink.ts +150 -0
- package/src/lib/tableColumns.ts +493 -0
- package/src/lib/tableEnv.ts +34 -0
- package/src/lib/tablePrefs.ts +84 -0
- package/src/lib/tags.ts +121 -12
- package/src/lib/themes.ts +286 -18
- package/src/lib/tour.ts +152 -0
- package/src/lib/trashEmpty.ts +111 -0
- package/src/lib/trashTimeLeft.ts +23 -0
- package/src/lib/uiLocales.ts +460 -0
- package/src/lib/viewPrefs.ts +696 -660
- package/src/lib/viewPrefsHttp.ts +78 -0
- package/src/locales/en.ts +515 -117
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +536 -152
- package/src/modals/ConvertModal.vue +62 -11
- package/src/modals/DestinationPickerModal.vue +59 -16
- package/src/modals/Modal.vue +113 -23
- package/src/modals/NewDocumentModal.vue +16 -6
- package/src/modals/NewFolderModal.vue +8 -0
- package/src/modals/PermissionsModal.vue +272 -163
- package/src/modals/PreviewModal.vue +119 -20
- package/src/modals/RenameModal.vue +37 -1
- package/src/styles/base.css +2890 -260
- package/src/styles/sign-fonts.css +118 -0
- package/src/styles/variables.css +60 -0
- package/src/types/Connections.ts +17 -0
- package/src/types/ExplorerConfig.ts +117 -1
- package/src/types/FileNode.ts +50 -0
- package/src/types/Plugins.ts +494 -0
- package/src/types/Public.ts +212 -0
- package/src/viewers/ArchiveViewer.vue +93 -48
- package/src/viewers/CsvViewer.vue +99 -53
- package/src/viewers/DrawioViewer.vue +35 -9
- package/src/viewers/EpubViewer.vue +7 -8
- package/src/viewers/IpynbViewer.vue +3 -1
- package/src/viewers/MermaidViewer.vue +4 -5
- package/src/viewers/PdfViewer.vue +8 -17
- package/src/viewers/PsdViewer.vue +2 -4
- package/src/viewers/TiffViewer.vue +4 -5
- package/src/viewers/Viewer3D.vue +1 -1
- package/LICENSE +0 -21
- package/dist/ArchiveViewer-BUQWLhwX.js +0 -124
- package/dist/ArchiveViewer-BUQWLhwX.js.map +0 -1
- package/dist/CsvViewer-Bg2_aqzc.js +0 -141
- package/dist/CsvViewer-Bg2_aqzc.js.map +0 -1
- package/dist/DrawioViewer-CC6rJXeP.js.map +0 -1
- package/dist/EpubViewer-CAoTSXTO.js.map +0 -1
- package/dist/IpynbViewer-D0GwGpnE.js +0 -184
- package/dist/IpynbViewer-D0GwGpnE.js.map +0 -1
- package/dist/MermaidViewer-DGwAvRZg.js +0 -137
- package/dist/MermaidViewer-DGwAvRZg.js.map +0 -1
- package/dist/PsdViewer-DhGweA0m.js +0 -122
- package/dist/PsdViewer-DhGweA0m.js.map +0 -1
- package/dist/TiffViewer-D0AurvAh.js +0 -142
- package/dist/TiffViewer-D0AurvAh.js.map +0 -1
- package/dist/Viewer3D-CdZeK4j4.js.map +0 -1
- package/dist/index-BArI-B1M.js +0 -26911
- package/dist/index-BArI-B1M.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
- package/src/composables/useScrolledX.ts +0 -21
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* uiLocales — the language list is not a constant any more.
|
|
3
|
+
*
|
|
4
|
+
* filex ships English and Turkish. A third source is an app — a LANGUAGE
|
|
5
|
+
* PACK — that carries a language for FILEX ITSELF (`manifest.ui_locales`):
|
|
6
|
+
* the server lists it, the interface offers it (public shell included),
|
|
7
|
+
* marks it with the app it came from, and drops it when the app is removed.
|
|
8
|
+
*
|
|
9
|
+
* ⚠⚠ TWO requests, not one. `GET /api/public/branding` LISTS the languages
|
|
10
|
+
* apps add — code, app, right-to-left or not — and carries no strings;
|
|
11
|
+
* `GET /api/public/ui-locales/{code}` is one language's strings, fetched the
|
|
12
|
+
* first time something asks for that language's table (`localeTable`,
|
|
13
|
+
* `ensureLocaleStrings`). A complete language is ~150 KB, and the branding
|
|
14
|
+
* answer is read by every public page and every panel start.
|
|
15
|
+
*
|
|
16
|
+
* ⚠⚠ The branding field used to be a MAP (`{"es": {key: text}}`) while this
|
|
17
|
+
* file read it as a LIST (`b.ui_locales?.length`): a map has no length, so an
|
|
18
|
+
* installed language joined the picker and then spoke English, and every test
|
|
19
|
+
* stayed green because its fixture was typed this file's way (measured
|
|
20
|
+
* 2026-09-21 with the Spanish pack). The web tests now read the SERVER's own
|
|
21
|
+
* bytes (backend/internal/api/handlers/testdata/wire/public-*.json).
|
|
22
|
+
*
|
|
23
|
+
* ⚠ A language that arrives this way may be a PARTIAL table. Every lookup
|
|
24
|
+
* falls back to English rather than printing a raw key — the rule the
|
|
25
|
+
* built-in catalogues already follow, applied to a table we did not write.
|
|
26
|
+
*
|
|
27
|
+
* ⚠ The strings come from outside. They are rendered as TEXT, never markup,
|
|
28
|
+
* and a key that could walk into an object's machinery (`__proto__`,
|
|
29
|
+
* `constructor`, `prototype`) is dropped here as well as refused by the host.
|
|
30
|
+
*
|
|
31
|
+
* ⚠⚠ Reactive on purpose. A pack can be installed or removed while the window
|
|
32
|
+
* is open, and its strings arrive after the page has painted; `localesVersion`
|
|
33
|
+
* is what every consumer watches, and `hasLocale` reads it so a computed that
|
|
34
|
+
* asked "is `es` offered?" asks again when the answer changes.
|
|
35
|
+
*/
|
|
36
|
+
import { ref } from 'vue';
|
|
37
|
+
import type { PublicBranding, PublicLocaleOption } from '../types/Public';
|
|
38
|
+
// ⚠ The two tables directly, not `../locales` (its index re-exports
|
|
39
|
+
// `locales/resolve`, which reads THIS module — an import cycle).
|
|
40
|
+
import { en } from '../locales/en';
|
|
41
|
+
import { tr } from '../locales/tr';
|
|
42
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
43
|
+
|
|
44
|
+
/** Bumped whenever the list or a table changes; `useLocale` and the pickers watch it. */
|
|
45
|
+
export const localesVersion = ref(0);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Has the server's list arrived? Until it has, a stored choice that LOOKS like
|
|
49
|
+
* a language (`plausibleLocale`) is held rather than thrown away — the same
|
|
50
|
+
* rule the palette follows for a custom theme whose list is still in flight
|
|
51
|
+
* (`lib/themes` plausibleThemeId). Without it a person who picked a pack's
|
|
52
|
+
* language opened every page in English: the stored `es` was judged against
|
|
53
|
+
* a list that had not been fetched yet, refused, and never looked at again.
|
|
54
|
+
*/
|
|
55
|
+
export const localesKnown = ref(false);
|
|
56
|
+
|
|
57
|
+
/** The languages this package ships a full catalogue for. */
|
|
58
|
+
export const BUILTIN_LOCALES: readonly LocaleCode[] = ['en', 'tr'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* A language's name IN that language — never translated into the viewer's,
|
|
62
|
+
* because somebody looking for their own language is looking for their own
|
|
63
|
+
* word for it. The table covers the common cases without asking `Intl`;
|
|
64
|
+
* anything else asks `Intl.DisplayNames` in its own language, and only a code
|
|
65
|
+
* even that cannot name is shown upper-cased.
|
|
66
|
+
*/
|
|
67
|
+
const BUILTIN_LABELS: Record<string, string> = {
|
|
68
|
+
en: 'English',
|
|
69
|
+
tr: 'Türkçe',
|
|
70
|
+
de: 'Deutsch',
|
|
71
|
+
fr: 'Français',
|
|
72
|
+
es: 'Español',
|
|
73
|
+
it: 'Italiano',
|
|
74
|
+
nl: 'Nederlands',
|
|
75
|
+
pt: 'Português',
|
|
76
|
+
pl: 'Polski',
|
|
77
|
+
ru: 'Русский',
|
|
78
|
+
uk: 'Українська',
|
|
79
|
+
ar: 'العربية',
|
|
80
|
+
fa: 'فارسی',
|
|
81
|
+
az: 'Azərbaycanca',
|
|
82
|
+
ja: '日本語',
|
|
83
|
+
ko: '한국어',
|
|
84
|
+
zh: '中文',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** A language's own name for itself. */
|
|
88
|
+
export function localeLabel(code: string): string {
|
|
89
|
+
if (BUILTIN_LABELS[code]) return BUILTIN_LABELS[code];
|
|
90
|
+
try {
|
|
91
|
+
const name = new Intl.DisplayNames([code], { type: 'language' }).of(code);
|
|
92
|
+
if (name && name.toLowerCase() !== code) {
|
|
93
|
+
// Upper-cased in the language's own rules ("español" → "Español").
|
|
94
|
+
return name.charAt(0).toLocaleUpperCase(code) + name.slice(1);
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
/* an old runtime or a tag Intl does not know */
|
|
98
|
+
}
|
|
99
|
+
return code.toUpperCase();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Everything a language added from outside carries. */
|
|
103
|
+
interface ExtraLocale extends Omit<PublicLocaleOption, 'strings'> {
|
|
104
|
+
/** `null` = listed, strings not fetched yet. */
|
|
105
|
+
strings: Record<string, string> | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const messages: Record<string, Record<string, string>> = { en, tr };
|
|
109
|
+
|
|
110
|
+
const extras = new Map<string, ExtraLocale>();
|
|
111
|
+
const inflight = new Map<string, Promise<boolean>>();
|
|
112
|
+
|
|
113
|
+
/* ── codes ────────────────────────────────────────────────────────────── */
|
|
114
|
+
|
|
115
|
+
const TAG = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Could this be a language tag at all? `es`, `pt-br`, `zh-hant` — yes;
|
|
119
|
+
* `../etc`, `` — no. What a HELD choice has to be.
|
|
120
|
+
*/
|
|
121
|
+
export function plausibleLocale(raw: string | undefined | null): boolean {
|
|
122
|
+
return TAG.test(String(raw ?? '').trim().toLowerCase().replace(/_/g, '-'));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The code the interface uses for a tag.
|
|
127
|
+
*
|
|
128
|
+
* `tr-TR` and `TR` mean `tr`; `es-MX` means `es` when a pack offers `es`;
|
|
129
|
+
* `pt` means `pt-br` when that is the only Portuguese on offer; a regional
|
|
130
|
+
* pack (`pt-br`, `zh-hant`) keeps its region, because Brazilian and European
|
|
131
|
+
* Portuguese — or the two Chinese scripts — are different tables. A tag
|
|
132
|
+
* nothing offers yet is KEPT whole, so a choice held before the list arrives
|
|
133
|
+
* still means what the person picked.
|
|
134
|
+
*/
|
|
135
|
+
export function normalizeLocaleCode(raw: string | undefined | null): string {
|
|
136
|
+
const v = String(raw ?? '').trim().toLowerCase().replace(/_/g, '-');
|
|
137
|
+
if (!TAG.test(v)) return '';
|
|
138
|
+
if (extras.has(v)) return v;
|
|
139
|
+
const primary = v.split('-')[0];
|
|
140
|
+
if (isBuiltinLocale(primary)) return primary;
|
|
141
|
+
if (extras.has(primary)) return primary;
|
|
142
|
+
for (const k of extras.keys()) if (k.startsWith(`${primary}-`)) return k;
|
|
143
|
+
return v;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function isBuiltinLocale(code: string): code is LocaleCode {
|
|
147
|
+
return (BUILTIN_LOCALES as readonly string[]).includes(code);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/* ── the registry ─────────────────────────────────────────────────────── */
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The strings a pack sent, minus anything that must not reach an object:
|
|
154
|
+
* non-strings, empty values (untranslated — English shows instead) and keys
|
|
155
|
+
* whose segments name JavaScript's machinery. The host refuses those keys at
|
|
156
|
+
* install already (wire.UILocaleKeyOK); this is the second lock, because the
|
|
157
|
+
* web SPA turns dotted keys into nested objects and `__proto__` would walk
|
|
158
|
+
* that loop into Object.prototype.
|
|
159
|
+
*/
|
|
160
|
+
function cleanStrings(raw: Record<string, unknown> | null | undefined): Record<string, string> {
|
|
161
|
+
const out: Record<string, string> = {};
|
|
162
|
+
for (const [k, v] of Object.entries(raw ?? {})) {
|
|
163
|
+
if (typeof v !== 'string' || !v.trim() || !k) continue;
|
|
164
|
+
if (k.split('.').some((s) => s === '__proto__' || s === 'constructor' || s === 'prototype')) continue;
|
|
165
|
+
out[k] = v;
|
|
166
|
+
}
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Add (or replace) a language the server offers. `strings` absent = listed
|
|
172
|
+
* only; the table is fetched when first asked for.
|
|
173
|
+
*
|
|
174
|
+
* ⚠ A built-in language is never replaced wholesale — a pack that ships
|
|
175
|
+
* `tr` is OVERLAYING keys onto the catalogue, not taking it over, or one
|
|
176
|
+
* badly-packed app could blank the interface for everybody who reads Turkish.
|
|
177
|
+
*/
|
|
178
|
+
export function registerLocale(opt: PublicLocaleOption): void {
|
|
179
|
+
const raw = String(opt?.code ?? '').trim().toLowerCase().replace(/_/g, '-');
|
|
180
|
+
const code = TAG.test(raw) ? raw : '';
|
|
181
|
+
if (!code) return;
|
|
182
|
+
extras.set(code, {
|
|
183
|
+
code,
|
|
184
|
+
label: String(opt.label ?? '').trim() || localeLabel(code),
|
|
185
|
+
source: opt.source === 'plugin' ? 'plugin' : opt.source || 'builtin',
|
|
186
|
+
plugin: opt.plugin,
|
|
187
|
+
rtl: opt.rtl === true,
|
|
188
|
+
strings: opt.strings ? cleanStrings(opt.strings) : null,
|
|
189
|
+
});
|
|
190
|
+
localesVersion.value += 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Replace the whole extra set (what one branding answer says). */
|
|
194
|
+
export function setLocales(list: PublicLocaleOption[] | undefined): void {
|
|
195
|
+
extras.clear();
|
|
196
|
+
inflight.clear();
|
|
197
|
+
for (const o of list ?? []) registerLocale(o);
|
|
198
|
+
localesVersion.value += 1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** An app was removed: its languages go with it. */
|
|
202
|
+
export function unregisterPluginLocales(plugin: string): void {
|
|
203
|
+
let changed = false;
|
|
204
|
+
for (const [code, v] of extras) {
|
|
205
|
+
if (v.source === 'plugin' && v.plugin === plugin) {
|
|
206
|
+
extras.delete(code);
|
|
207
|
+
changed = true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (changed) localesVersion.value += 1;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Testing seam / sign-out: back to the two we ship, list not yet known. */
|
|
214
|
+
export function resetLocales(): void {
|
|
215
|
+
extras.clear();
|
|
216
|
+
inflight.clear();
|
|
217
|
+
source = {};
|
|
218
|
+
localesKnown.value = false;
|
|
219
|
+
localesVersion.value += 1;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Every language on offer, built-ins first and the rest in the order the
|
|
224
|
+
* server listed them — so the two the product ships do not move around when
|
|
225
|
+
* an app is installed. ⚠ THE one list every picker reads (the admin
|
|
226
|
+
* header's switcher, the settings dialog, the public shell).
|
|
227
|
+
*/
|
|
228
|
+
export function availableLocales(): PublicLocaleOption[] {
|
|
229
|
+
void localesVersion.value;
|
|
230
|
+
const out: PublicLocaleOption[] = BUILTIN_LOCALES.map((code) => ({
|
|
231
|
+
code,
|
|
232
|
+
label: localeLabel(code),
|
|
233
|
+
source: 'builtin',
|
|
234
|
+
}));
|
|
235
|
+
for (const [code, v] of extras) {
|
|
236
|
+
if (isBuiltinLocale(code)) continue;
|
|
237
|
+
out.push({ code, label: v.label, source: v.source, plugin: v.plugin, rtl: v.rtl });
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Is this a language the interface can actually be set to? (Reactive.) */
|
|
243
|
+
export function hasLocale(code: string | undefined | null): boolean {
|
|
244
|
+
void localesVersion.value;
|
|
245
|
+
const c = normalizeLocaleCode(code);
|
|
246
|
+
return !!c && (isBuiltinLocale(c) || extras.has(c));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* May a STORED choice stand? Offered, or — while the list has not arrived —
|
|
251
|
+
* merely plausible. After the list arrives only an offered language stands;
|
|
252
|
+
* the stored value itself is kept, so reinstalling the pack brings it back.
|
|
253
|
+
*/
|
|
254
|
+
export function acceptableLocale(code: string | undefined | null): boolean {
|
|
255
|
+
if (hasLocale(code)) return true;
|
|
256
|
+
return !localesKnown.value && plausibleLocale(code);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ── tables ───────────────────────────────────────────────────────────── */
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* One language's table with English merged underneath.
|
|
263
|
+
*
|
|
264
|
+
* Order, and why: English underneath as the last resort, then the built-in
|
|
265
|
+
* catalogue when the language has one, then the pack's table on top. So a
|
|
266
|
+
* pack that translates three strings gets three strings and an otherwise
|
|
267
|
+
* English screen — never a page of raw keys — and a pack that overlays two
|
|
268
|
+
* Turkish strings changes exactly two.
|
|
269
|
+
*
|
|
270
|
+
* ⚠ Not what `t()` reads any more — see `localeOwnTable`. In this merge a
|
|
271
|
+
* key the pack never wrote and a key it did are indistinguishable, and the
|
|
272
|
+
* plural rule needs to tell them apart.
|
|
273
|
+
*
|
|
274
|
+
* ⚠ Asking for a language whose strings have not been fetched STARTS the
|
|
275
|
+
* fetch and answers English meanwhile; `localesVersion` moves when they land,
|
|
276
|
+
* and every computed that read this re-reads it.
|
|
277
|
+
*/
|
|
278
|
+
export function localeTable(code: string | undefined): Record<string, string> {
|
|
279
|
+
const c = normalizeLocaleCode(code);
|
|
280
|
+
const builtin = isBuiltinLocale(c) ? messages[c] : undefined;
|
|
281
|
+
const extra = extras.get(c);
|
|
282
|
+
if (extra && extra.strings === null) void ensureLocaleStrings(c);
|
|
283
|
+
if (!extra?.strings || !Object.keys(extra.strings).length) return builtin ?? messages.en;
|
|
284
|
+
return { ...messages.en, ...(builtin ?? {}), ...extra.strings };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** What a language wrote ITSELF, and whose plural rules read it. */
|
|
288
|
+
export interface OwnLocaleTable {
|
|
289
|
+
/** The language whose CLDR plural rules pick a form out of `strings`. */
|
|
290
|
+
lang: string;
|
|
291
|
+
/** Its own words only — English is NOT merged underneath. */
|
|
292
|
+
strings: Readonly<Record<string, string>>;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The words a language has of its OWN — what `t()` reads first.
|
|
297
|
+
*
|
|
298
|
+
* - a built-in (`en`, `tr`): its catalogue, with a pack that overlays it on
|
|
299
|
+
* top (the overlay IS that language's words);
|
|
300
|
+
* - a pack's language: the pack's strings and nothing else;
|
|
301
|
+
* - a language with no strings yet (not fetched, fetch failed, empty pack,
|
|
302
|
+
* or a code nothing offers): English, AS English — `lang` is `en`.
|
|
303
|
+
*
|
|
304
|
+
* ⚠⚠ Why not `localeTable`. That one merges English under the pack, so "the
|
|
305
|
+
* table has `toast.restored_one`" is true for a Spanish pack that wrote only
|
|
306
|
+
* `toast.restored` — the English singular was sitting underneath — and the
|
|
307
|
+
* count of one printed "1 item restored" in the middle of a Spanish screen.
|
|
308
|
+
* The plural rule has to know which forms the language itself wrote, and
|
|
309
|
+
* fall back to English as a WHOLE (English's form, by English's rule) only
|
|
310
|
+
* when the language wrote none.
|
|
311
|
+
*
|
|
312
|
+
* ⚠ The last case answers `lang: 'en'` on purpose: French puts 0 in `one`,
|
|
313
|
+
* so reading English strings by French rules would print "0 item".
|
|
314
|
+
*
|
|
315
|
+
* Starts the fetch for a listed language whose strings are not in hand, the
|
|
316
|
+
* same as `localeTable`.
|
|
317
|
+
*/
|
|
318
|
+
export function localeOwnTable(code: string | undefined): OwnLocaleTable {
|
|
319
|
+
const c = normalizeLocaleCode(code);
|
|
320
|
+
const builtin = isBuiltinLocale(c) ? messages[c] : undefined;
|
|
321
|
+
const extra = extras.get(c);
|
|
322
|
+
if (extra && extra.strings === null) void ensureLocaleStrings(c);
|
|
323
|
+
const pack = extra?.strings && Object.keys(extra.strings).length ? extra.strings : null;
|
|
324
|
+
if (builtin) return { lang: c, strings: pack ? { ...builtin, ...pack } : builtin };
|
|
325
|
+
if (pack) return { lang: c, strings: pack };
|
|
326
|
+
return { lang: 'en', strings: messages.en };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Just the strings a pack added for this language — no catalogue under them.
|
|
331
|
+
*
|
|
332
|
+
* ⚠ For a host with a catalogue of its own (the admin SPA's `web/src/locales`)
|
|
333
|
+
* that merges the same table into ITS `t()`. Handing it `localeTable` would
|
|
334
|
+
* fold this package's keys into the host's, which is two catalogues becoming
|
|
335
|
+
* one by accident.
|
|
336
|
+
*/
|
|
337
|
+
export function localeStrings(code: string | undefined): Record<string, string> {
|
|
338
|
+
return { ...(extras.get(normalizeLocaleCode(code))?.strings ?? {}) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* ── fetching ─────────────────────────────────────────────────────────── */
|
|
342
|
+
|
|
343
|
+
export interface LoadLocalesOptions {
|
|
344
|
+
/** API origin; empty = same origin. */
|
|
345
|
+
base?: string;
|
|
346
|
+
fetchImpl?: typeof fetch;
|
|
347
|
+
/** Per-call auth for a host that does not ride on cookies. */
|
|
348
|
+
headers?: () => Promise<Record<string, string>> | Record<string, string>;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** Where the list came from — the strings are fetched from the same place. */
|
|
352
|
+
let source: LoadLocalesOptions = {};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Fetch one added language's strings (`GET /api/public/ui-locales/{code}`).
|
|
356
|
+
* Resolves true when a table is in hand. Idempotent: concurrent askers share
|
|
357
|
+
* one request, and a language already loaded costs nothing.
|
|
358
|
+
*
|
|
359
|
+
* ⚠ A failure leaves the language listed with an EMPTY table (English shows)
|
|
360
|
+
* rather than retrying on every repaint — a pack removed between the list and
|
|
361
|
+
* this request answers 404, and hammering the server over it helps nobody.
|
|
362
|
+
*/
|
|
363
|
+
export function ensureLocaleStrings(code: string | undefined): Promise<boolean> {
|
|
364
|
+
const c = normalizeLocaleCode(code);
|
|
365
|
+
const e = extras.get(c);
|
|
366
|
+
if (!e) return Promise.resolve(false);
|
|
367
|
+
if (e.strings !== null) return Promise.resolve(Object.keys(e.strings).length > 0);
|
|
368
|
+
const pending = inflight.get(c);
|
|
369
|
+
if (pending) return pending;
|
|
370
|
+
const doFetch = source.fetchImpl ?? (typeof fetch === 'function' ? fetch : null);
|
|
371
|
+
if (!doFetch) return Promise.resolve(false);
|
|
372
|
+
const base = (source.base ?? '').replace(/\/+$/, '');
|
|
373
|
+
const p = (async () => {
|
|
374
|
+
let strings: Record<string, string> = {};
|
|
375
|
+
try {
|
|
376
|
+
const extra = source.headers ? await source.headers() : {};
|
|
377
|
+
const res = await doFetch(`${base}/api/public/ui-locales/${encodeURIComponent(c)}`, {
|
|
378
|
+
method: 'GET',
|
|
379
|
+
headers: { Accept: 'application/json', ...extra },
|
|
380
|
+
credentials: 'same-origin',
|
|
381
|
+
});
|
|
382
|
+
if (res.ok) {
|
|
383
|
+
const body = (await res.json()) as { strings?: Record<string, unknown> };
|
|
384
|
+
strings = cleanStrings(body?.strings);
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
/* English is a perfectly good answer */
|
|
388
|
+
}
|
|
389
|
+
const still = extras.get(c);
|
|
390
|
+
if (still === e) {
|
|
391
|
+
still.strings = strings;
|
|
392
|
+
localesVersion.value += 1;
|
|
393
|
+
}
|
|
394
|
+
inflight.delete(c);
|
|
395
|
+
return Object.keys(strings).length > 0;
|
|
396
|
+
})();
|
|
397
|
+
inflight.set(c, p);
|
|
398
|
+
return p;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Fold a branding answer's language list into the registry.
|
|
403
|
+
*
|
|
404
|
+
* `locales` is a list of CODES — the built-ins and whatever apps add — and
|
|
405
|
+
* `ui_locales` the added ones as rows (code, app, rtl) WITHOUT strings.
|
|
406
|
+
* ⚠ Registering again drops tables already fetched, so a pack that was
|
|
407
|
+
* upgraded while the page was open is re-read the next time it is used
|
|
408
|
+
* instead of serving yesterday's words until a reload.
|
|
409
|
+
*/
|
|
410
|
+
export function setLocalesFromBranding(
|
|
411
|
+
b: PublicBranding | null | undefined,
|
|
412
|
+
opts?: LoadLocalesOptions,
|
|
413
|
+
): PublicLocaleOption[] {
|
|
414
|
+
if (opts) source = opts;
|
|
415
|
+
if (!b) return availableLocales();
|
|
416
|
+
const rows = Array.isArray(b.ui_locales) ? b.ui_locales : [];
|
|
417
|
+
const listed = new Set(rows.map((r) => String(r?.code ?? '').toLowerCase()));
|
|
418
|
+
// A code on `locales` with no row (an older server) still gets a name.
|
|
419
|
+
const bare = (b.locales ?? [])
|
|
420
|
+
.map((c) => String(c ?? '').trim().toLowerCase())
|
|
421
|
+
.filter((c) => TAG.test(c) && !isBuiltinLocale(c) && !listed.has(c))
|
|
422
|
+
.map((code) => ({ code, source: 'plugin' as const }));
|
|
423
|
+
setLocales([...rows, ...bare]);
|
|
424
|
+
localesKnown.value = true;
|
|
425
|
+
return availableLocales();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Ask the server which languages this instance offers.
|
|
430
|
+
*
|
|
431
|
+
* ⚠ It asks the BRANDING route, which is the one public answer about how
|
|
432
|
+
* this instance presents itself — and the one the public shell already
|
|
433
|
+
* fetches. ⚠ A failure is not an error: the two built-in languages are
|
|
434
|
+
* already on the picker, and the list is then treated as KNOWN (a held
|
|
435
|
+
* choice falls back) — waiting forever for an answer that will not come
|
|
436
|
+
* would keep somebody's interface in a language nothing can serve.
|
|
437
|
+
*/
|
|
438
|
+
export async function loadLocales(opts: LoadLocalesOptions = {}): Promise<PublicLocaleOption[]> {
|
|
439
|
+
source = opts;
|
|
440
|
+
const doFetch = opts.fetchImpl ?? (typeof fetch === 'function' ? fetch : null);
|
|
441
|
+
if (!doFetch) return availableLocales();
|
|
442
|
+
const base = (opts.base ?? '').replace(/\/+$/, '');
|
|
443
|
+
try {
|
|
444
|
+
const extra = opts.headers ? await opts.headers() : {};
|
|
445
|
+
const res = await doFetch(`${base}/api/public/branding`, {
|
|
446
|
+
method: 'GET',
|
|
447
|
+
headers: { Accept: 'application/json', ...extra },
|
|
448
|
+
credentials: 'same-origin',
|
|
449
|
+
});
|
|
450
|
+
if (res.ok) {
|
|
451
|
+
setLocalesFromBranding((await res.json()) as PublicBranding);
|
|
452
|
+
return availableLocales();
|
|
453
|
+
}
|
|
454
|
+
} catch {
|
|
455
|
+
/* the two built-in languages are a perfectly good answer */
|
|
456
|
+
}
|
|
457
|
+
localesKnown.value = true;
|
|
458
|
+
localesVersion.value += 1;
|
|
459
|
+
return availableLocales();
|
|
460
|
+
}
|