@kernhq/module-inventory 0.1.2 → 0.3.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 +134 -9
- package/dist/contract/capabilities.d.ts +49 -0
- package/dist/contract/capabilities.d.ts.map +1 -0
- package/dist/contract/capabilities.js +94 -0
- package/dist/contract/capabilities.js.map +1 -0
- package/dist/contract/events.d.ts +76 -0
- package/dist/contract/events.d.ts.map +1 -0
- package/dist/contract/events.js +62 -0
- package/dist/contract/events.js.map +1 -0
- package/dist/contract/index.d.ts +16 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +16 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/models.d.ts +468 -0
- package/dist/contract/models.d.ts.map +1 -0
- package/dist/contract/models.js +320 -0
- package/dist/contract/models.js.map +1 -0
- package/dist/contract/notifications.d.ts +24 -0
- package/dist/contract/notifications.d.ts.map +1 -0
- package/dist/contract/notifications.js +72 -0
- package/dist/contract/notifications.js.map +1 -0
- package/dist/contract/permissions.d.ts +72 -0
- package/dist/contract/permissions.d.ts.map +1 -0
- package/dist/contract/permissions.js +79 -0
- package/dist/contract/permissions.js.map +1 -0
- package/dist/contract/router.d.ts +1378 -0
- package/dist/contract/router.d.ts.map +1 -0
- package/dist/contract/router.js +266 -0
- package/dist/contract/router.js.map +1 -0
- package/dist/contract/settings.d.ts +20 -0
- package/dist/contract/settings.d.ts.map +1 -0
- package/dist/contract/settings.js +49 -0
- package/dist/contract/settings.js.map +1 -0
- package/dist/server/index.d.ts +8 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +288 -8
- package/dist/server/index.js.map +1 -1
- package/dist/server/jobs.d.ts +48 -0
- package/dist/server/jobs.d.ts.map +1 -0
- package/dist/server/jobs.js +358 -0
- package/dist/server/jobs.js.map +1 -0
- package/dist/server/router.d.ts +1789 -0
- package/dist/server/router.d.ts.map +1 -0
- package/dist/server/router.js +439 -0
- package/dist/server/router.js.map +1 -0
- package/dist/server/schema.d.ts +156 -9
- package/dist/server/schema.d.ts.map +1 -1
- package/dist/server/schema.js +183 -11
- package/dist/server/schema.js.map +1 -1
- package/dist/server/services/assets.d.ts +139 -0
- package/dist/server/services/assets.d.ts.map +1 -0
- package/dist/server/services/assets.js +429 -0
- package/dist/server/services/assets.js.map +1 -0
- package/dist/server/services/attachments.d.ts +80 -0
- package/dist/server/services/attachments.d.ts.map +1 -0
- package/dist/server/services/attachments.js +182 -0
- package/dist/server/services/attachments.js.map +1 -0
- package/dist/server/services/audience.d.ts +15 -0
- package/dist/server/services/audience.d.ts.map +1 -0
- package/dist/server/services/audience.js +64 -0
- package/dist/server/services/audience.js.map +1 -0
- package/dist/server/services/categories.d.ts +57 -0
- package/dist/server/services/categories.d.ts.map +1 -0
- package/dist/server/services/categories.js +124 -0
- package/dist/server/services/categories.js.map +1 -0
- package/dist/server/services/cursor.d.ts +68 -0
- package/dist/server/services/cursor.d.ts.map +1 -0
- package/dist/server/services/cursor.js +39 -0
- package/dist/server/services/cursor.js.map +1 -0
- package/dist/server/services/custody.d.ts +175 -0
- package/dist/server/services/custody.d.ts.map +1 -0
- package/dist/server/services/custody.js +367 -0
- package/dist/server/services/custody.js.map +1 -0
- package/dist/server/services/db-errors.d.ts +7 -0
- package/dist/server/services/db-errors.d.ts.map +1 -0
- package/dist/server/services/db-errors.js +32 -0
- package/dist/server/services/db-errors.js.map +1 -0
- package/dist/server/services/index.d.ts +26 -0
- package/dist/server/services/index.d.ts.map +1 -0
- package/dist/server/services/index.js +39 -0
- package/dist/server/services/index.js.map +1 -0
- package/dist/server/services/members.d.ts +27 -0
- package/dist/server/services/members.d.ts.map +1 -0
- package/dist/server/services/members.js +39 -0
- package/dist/server/services/members.js.map +1 -0
- package/dist/server/services/notify.d.ts +105 -0
- package/dist/server/services/notify.d.ts.map +1 -0
- package/dist/server/services/notify.js +147 -0
- package/dist/server/services/notify.js.map +1 -0
- package/dist/server/services/offboarding.d.ts +70 -0
- package/dist/server/services/offboarding.d.ts.map +1 -0
- package/dist/server/services/offboarding.js +116 -0
- package/dist/server/services/offboarding.js.map +1 -0
- package/dist/server/services/repairs.d.ts +204 -0
- package/dist/server/services/repairs.d.ts.map +1 -0
- package/dist/server/services/repairs.js +476 -0
- package/dist/server/services/repairs.js.map +1 -0
- package/dist/server/services/search.d.ts +85 -0
- package/dist/server/services/search.d.ts.map +1 -0
- package/dist/server/services/search.js +142 -0
- package/dist/server/services/search.js.map +1 -0
- package/dist/server/services/stats.d.ts +42 -0
- package/dist/server/services/stats.d.ts.map +1 -0
- package/dist/server/services/stats.js +80 -0
- package/dist/server/services/stats.js.map +1 -0
- package/dist/server/services/status.d.ts +102 -0
- package/dist/server/services/status.d.ts.map +1 -0
- package/dist/server/services/status.js +71 -0
- package/dist/server/services/status.js.map +1 -0
- package/migrations/0000_init.sql +12 -3
- package/migrations/0001_rls.sql +24 -0
- package/migrations/0002_custody_and_categories.sql +23 -0
- package/migrations/0003_repairs.sql +23 -0
- package/migrations/0004_platform_surfaces.sql +51 -0
- package/migrations/0005_repair_dates.sql +35 -0
- package/migrations/0006_workspace_registry_read.sql +50 -0
- package/migrations/0007_history_sequence.sql +83 -0
- package/migrations/meta/0000_snapshot.json +40 -13
- package/migrations/meta/0002_snapshot.json +1054 -0
- package/migrations/meta/0003_snapshot.json +1070 -0
- package/migrations/meta/0004_snapshot.json +1130 -0
- package/migrations/meta/0005_snapshot.json +1135 -0
- package/migrations/meta/_journal.json +44 -2
- package/package.json +5 -4
- package/src/client/api-instance.ts +27 -2
- package/src/client/api.ts +1 -1
- package/src/client/bidi.test.ts +148 -0
- package/src/client/bidi.ts +85 -0
- package/src/client/components/AssetDetailPanel.svelte +614 -0
- package/src/client/components/AssetFormDialog.svelte +191 -59
- package/src/client/components/AssetPhoto.svelte +178 -0
- package/src/client/components/AttachmentsSection.svelte +327 -0
- package/src/client/components/CustodyDialog.svelte +201 -0
- package/src/client/components/RepairDialog.svelte +271 -0
- package/src/client/components/RepairsSection.svelte +318 -0
- package/src/client/components/Timeline.svelte +347 -0
- package/src/client/components/TimelineText.svelte +124 -0
- package/src/client/core-api.ts +71 -0
- package/src/client/custody.test.ts +31 -0
- package/src/client/custody.ts +34 -0
- package/src/client/errors.test.ts +365 -0
- package/src/client/errors.ts +201 -0
- package/src/client/i18n.ts +11 -166
- package/src/client/index.ts +8 -1
- package/src/client/links.test.ts +74 -0
- package/src/client/links.ts +44 -0
- package/src/client/members.test.ts +132 -0
- package/src/client/members.ts +116 -0
- package/src/client/messages.test.ts +296 -0
- package/src/client/messages.ts +1424 -0
- package/src/client/mock.test.ts +555 -0
- package/src/client/mock.ts +1261 -52
- package/src/client/module.ts +76 -2
- package/src/client/pages/AssetsPage.svelte +638 -145
- package/src/client/permissions.ts +8 -1
- package/src/client/price.test.ts +254 -0
- package/src/client/price.ts +279 -0
- package/src/client/query.test.ts +58 -0
- package/src/client/query.ts +51 -2
- package/src/client/repairs.test.ts +38 -0
- package/src/client/repairs.ts +38 -0
- package/src/client/settings/CategoriesSettings.svelte +421 -0
- package/src/client/settings/GeneralSettings.svelte +403 -0
- package/src/client/status.ts +29 -0
- package/src/client/timeline.test.ts +175 -0
- package/src/client/timeline.ts +206 -0
- package/src/client/widgets/OverviewWidget.svelte +140 -26
- package/src/client/widgets/RepairsWidget.svelte +124 -0
- package/src/contract/capabilities.ts +99 -0
- package/src/contract/events.ts +83 -0
- package/src/contract/index.ts +16 -0
- package/src/contract/models.ts +360 -0
- package/src/contract/notifications.ts +73 -0
- package/src/contract/permissions.ts +79 -0
- package/src/contract/router.ts +300 -0
- package/src/contract/settings.ts +50 -0
- package/src/module.test.ts +330 -7
- package/src/server/index.ts +318 -8
- package/src/server/inventory.int.test.ts +4374 -0
- package/src/server/jobs.ts +444 -0
- package/src/server/migrations.test.ts +251 -0
- package/src/server/router.ts +574 -0
- package/src/server/schema.ts +184 -10
- package/src/server/services/assets.ts +528 -0
- package/src/server/services/attachments.ts +215 -0
- package/src/server/services/audience.ts +77 -0
- package/src/server/services/categories.ts +136 -0
- package/src/server/services/cursor.ts +104 -0
- package/src/server/services/custody.ts +471 -0
- package/src/server/services/db-errors.ts +42 -0
- package/src/server/services/index.ts +56 -0
- package/src/server/services/members.ts +54 -0
- package/src/server/services/notify.ts +196 -0
- package/src/server/services/offboarding.ts +150 -0
- package/src/server/services/repairs.ts +567 -0
- package/src/server/services/search.ts +166 -0
- package/src/server/services/stats.ts +88 -0
- package/src/server/services/status.test.ts +34 -0
- package/src/server/services/status.ts +143 -0
- package/tsconfig.base.json +22 -0
- package/tsconfig.client.json +1 -1
- package/tsconfig.json +1 -1
- package/vitest.config.ts +18 -3
- package/dist/contract.d.ts +0 -387
- package/dist/contract.d.ts.map +0 -1
- package/dist/contract.js +0 -119
- package/dist/contract.js.map +0 -1
- package/dist/server/_impl.d.ts +0 -427
- package/dist/server/_impl.d.ts.map +0 -1
- package/dist/server/_impl.js +0 -204
- package/dist/server/_impl.js.map +0 -1
- package/src/contract.ts +0 -143
- package/src/server/_impl.ts +0 -275
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { ar, de, en, fa, inventoryMessageBundles, tr } from './messages.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The bundles, structurally — and the counted messages, behaviourally.
|
|
9
|
+
*
|
|
10
|
+
* Nothing else looks at these. A key present in English and missing in Persian type-checks, builds,
|
|
11
|
+
* lints and ships, and the first person to see it is the one reading `inventory.status_lost` on a
|
|
12
|
+
* screen. Adding a status to `AssetStatus` and forgetting four translations is the exact shape of
|
|
13
|
+
* that mistake, and this file is what makes it fail here instead.
|
|
14
|
+
*/
|
|
15
|
+
const BUNDLES = { en, ar, de, fa, tr } as const
|
|
16
|
+
type Locale = keyof typeof BUNDLES
|
|
17
|
+
|
|
18
|
+
/** The count placeholder: the runtime accepts `count` or `n`, and this module's catalogue uses `n`. */
|
|
19
|
+
const COUNT = 'n'
|
|
20
|
+
const placeholders = (s: string) => new Set([...s.matchAll(/\{(\w+)\}/g)].map((m) => m[1]!))
|
|
21
|
+
const forms = (v: unknown): string[] => (typeof v === 'string' ? [v] : Object.values(v as object))
|
|
22
|
+
|
|
23
|
+
describe('bundles', () => {
|
|
24
|
+
it('declares every locale the module claims to ship', () => {
|
|
25
|
+
expect(Object.keys(inventoryMessageBundles).sort()).toEqual(['ar', 'de', 'en', 'fa', 'tr'])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('has the same key set in every locale', () => {
|
|
29
|
+
const keys = Object.keys(en).sort()
|
|
30
|
+
for (const [locale, bundle] of Object.entries(BUNDLES)) {
|
|
31
|
+
expect({ locale, keys: Object.keys(bundle).sort() }).toEqual({ locale, keys })
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('namespaces every key, so two modules cannot collide in the merged map', () => {
|
|
36
|
+
for (const key of Object.keys(en)) expect(key.startsWith('inventory.')).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A plural form may drop the numeral — "one asset" reads better than "1 asset" in some
|
|
41
|
+
* languages — but dropping any *other* placeholder loses information the sentence needed.
|
|
42
|
+
*/
|
|
43
|
+
it('keeps every non-count placeholder in every plural form', () => {
|
|
44
|
+
for (const [key, value] of Object.entries(en)) {
|
|
45
|
+
const expected = placeholders(forms(value).join(' '))
|
|
46
|
+
expected.delete(COUNT)
|
|
47
|
+
for (const [locale, bundle] of Object.entries(BUNDLES)) {
|
|
48
|
+
for (const form of forms(bundle[key as keyof typeof bundle])) {
|
|
49
|
+
const got = placeholders(form)
|
|
50
|
+
got.delete(COUNT)
|
|
51
|
+
expect({ key, locale, form, got: [...got].sort() }).toEqual({
|
|
52
|
+
key,
|
|
53
|
+
locale,
|
|
54
|
+
form,
|
|
55
|
+
got: [...expected].sort(),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('never invents a placeholder the English string does not have', () => {
|
|
63
|
+
for (const [key, value] of Object.entries(en)) {
|
|
64
|
+
const allowed = placeholders(forms(value).join(' '))
|
|
65
|
+
for (const [locale, bundle] of Object.entries(BUNDLES))
|
|
66
|
+
for (const form of forms(bundle[key as keyof typeof bundle]))
|
|
67
|
+
for (const name of placeholders(form))
|
|
68
|
+
expect({ key, locale, name, known: allowed.has(name) }).toEqual({
|
|
69
|
+
key,
|
|
70
|
+
locale,
|
|
71
|
+
name,
|
|
72
|
+
known: true,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
/** Arabic inflects six ways. A bundle with only one/other picks `other` for two, and reads wrong. */
|
|
78
|
+
it('gives every Arabic plural all six CLDR categories', () => {
|
|
79
|
+
const wanted = new Intl.PluralRules('ar').resolvedOptions().pluralCategories.sort()
|
|
80
|
+
for (const [key, value] of Object.entries(ar)) {
|
|
81
|
+
if (typeof value === 'string') continue
|
|
82
|
+
expect({ key, cats: Object.keys(value).sort() }).toEqual({ key, cats: wanted })
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The slip that six forms invite: writing five of them and abbreviating the sixth.
|
|
88
|
+
*
|
|
89
|
+
* `many` is the form for 11–99, where Arabic puts the counted noun in the singular accusative —
|
|
90
|
+
* "{n} أصلاً". Because that form is written *differently* from `other`, it gets written from
|
|
91
|
+
* scratch rather than copied, and a compound noun loses half of itself on the way: the shell's
|
|
92
|
+
* `modules_meta_objects` said "{n} نوعاً" — "{n} type" — where every other form said "نوع كائن",
|
|
93
|
+
* "object type". Nothing catches that. It compiles, it has all six categories, it has the right
|
|
94
|
+
* placeholder, and it is missing the word the sentence is about.
|
|
95
|
+
*
|
|
96
|
+
* Word count against `other` is a heuristic and is named as one: a form with *fewer* words than
|
|
97
|
+
* `other` has dropped one, and in a counted phrase the word it drops is the noun. Inflection
|
|
98
|
+
* changes a word's letters, not how many words there are, so this stays true across all six.
|
|
99
|
+
*/
|
|
100
|
+
it('never drops a word from an Arabic plural form that the `other` form has', () => {
|
|
101
|
+
const words = (form: string) => form.split(/\s+/).filter((word) => word && !word.includes('{')).length
|
|
102
|
+
for (const [key, value] of Object.entries(ar)) {
|
|
103
|
+
if (typeof value === 'string') continue
|
|
104
|
+
const other = (value as Record<string, string>).other ?? ''
|
|
105
|
+
for (const [category, form] of Object.entries(value as Record<string, string>)) {
|
|
106
|
+
// `zero` is a negation — "لا أصول" — and is the one form entitled to a different shape.
|
|
107
|
+
if (category === 'zero') continue
|
|
108
|
+
expect({ key, category, form, atLeast: words(other) }).toEqual({
|
|
109
|
+
key,
|
|
110
|
+
category,
|
|
111
|
+
form,
|
|
112
|
+
atLeast: Math.min(words(form), words(other)),
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('makes a plural of a key wherever English has one', () => {
|
|
119
|
+
for (const [key, value] of Object.entries(en)) {
|
|
120
|
+
if (typeof value === 'string') continue
|
|
121
|
+
for (const [locale, bundle] of Object.entries(BUNDLES))
|
|
122
|
+
expect({ key, locale, plural: typeof bundle[key as keyof typeof bundle] }).toEqual({
|
|
123
|
+
key,
|
|
124
|
+
locale,
|
|
125
|
+
plural: 'object',
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// ------------------------------------------------------ keys nothing asks for any more
|
|
132
|
+
|
|
133
|
+
const HERE = dirname(fileURLToPath(import.meta.url))
|
|
134
|
+
|
|
135
|
+
/** Everything in this client that could name a key — components included, catalogue excluded. */
|
|
136
|
+
function clientSource(): string {
|
|
137
|
+
const files = readdirSync(HERE, { recursive: true, encoding: 'utf8' }).filter(
|
|
138
|
+
(file) =>
|
|
139
|
+
(file.endsWith('.ts') || file.endsWith('.svelte')) &&
|
|
140
|
+
!file.endsWith('.test.ts') &&
|
|
141
|
+
!file.endsWith('messages.ts'),
|
|
142
|
+
)
|
|
143
|
+
return files.map((file) => readFileSync(join(HERE, file), 'utf8')).join('\n')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The two families of key that are *built* rather than written, and so are never literals.
|
|
148
|
+
*
|
|
149
|
+
* `t(`status_${status}`)` and `actionKey()`'s `history_${action}`. Both lists are duplicated here
|
|
150
|
+
* on purpose — the file is data-only and importing the contract to check a catalogue would be a
|
|
151
|
+
* circular sort of proof — and `status labels` below already holds the first one to the contract.
|
|
152
|
+
*/
|
|
153
|
+
const BUILT = new Set([
|
|
154
|
+
...['in_stock', 'assigned', 'reserved', 'under_repair', 'lost', 'retired'].map((s) => `status_${s}`),
|
|
155
|
+
...[
|
|
156
|
+
'created',
|
|
157
|
+
'updated',
|
|
158
|
+
'assigned',
|
|
159
|
+
'transferred',
|
|
160
|
+
'returned',
|
|
161
|
+
'retired',
|
|
162
|
+
'restored',
|
|
163
|
+
'repair_logged',
|
|
164
|
+
'repair_completed',
|
|
165
|
+
'attachment_added',
|
|
166
|
+
'attachment_removed',
|
|
167
|
+
].map((a) => `history_${a}`),
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
describe('every key is one something asks for', () => {
|
|
171
|
+
/**
|
|
172
|
+
* `inventory.settings_save_error` — "The settings could not be saved" — sat in all five bundles
|
|
173
|
+
* with nothing reading it: the save bar renders `errorMessage(err, t)`, which names the actual
|
|
174
|
+
* refusal instead. A dead key is five translations somebody maintains, reviews and re-reads for
|
|
175
|
+
* a sentence that cannot appear, and nothing about it looks wrong from inside the catalogue.
|
|
176
|
+
*
|
|
177
|
+
* Literals rather than a runtime trace, because a `.svelte` file cannot be unit-tested here —
|
|
178
|
+
* the same reason `errors.test.ts` reads `src/server` off disk. It misses a key that happens to
|
|
179
|
+
* appear as an unrelated string somewhere, which is a false *pass*; it never fails a key that is
|
|
180
|
+
* really used, which is the direction that matters for a check nobody wants to fight.
|
|
181
|
+
*/
|
|
182
|
+
it('has nothing in the catalogue that no screen can reach', () => {
|
|
183
|
+
const source = clientSource()
|
|
184
|
+
const dead = Object.keys(en)
|
|
185
|
+
.map((key) => key.slice('inventory.'.length))
|
|
186
|
+
.filter((name) => !BUILT.has(name))
|
|
187
|
+
.filter((name) => !(source.includes(`'${name}'`) || source.includes(`"${name}"`)))
|
|
188
|
+
expect(dead).toEqual([])
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('is reading the client rather than passing on an empty sweep', () => {
|
|
192
|
+
expect(clientSource().length).toBeGreaterThan(10_000)
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Every status the contract can hold has a label, in every language.
|
|
198
|
+
*
|
|
199
|
+
* The screens render a status with `t(`status_${status}`)`, which cannot be type-checked: a status
|
|
200
|
+
* added to the enum and not to the catalogue renders the literal key `inventory.status_lost` in the
|
|
201
|
+
* middle of a table, in every locale at once.
|
|
202
|
+
*/
|
|
203
|
+
describe('status labels', () => {
|
|
204
|
+
// Not imported from the contract: this file is data-only on purpose, and a copy of six strings
|
|
205
|
+
// that must match is exactly what a test is for.
|
|
206
|
+
const STATUSES = ['in_stock', 'assigned', 'reserved', 'under_repair', 'lost', 'retired']
|
|
207
|
+
|
|
208
|
+
it('labels every asset status in every locale', () => {
|
|
209
|
+
for (const status of STATUSES)
|
|
210
|
+
for (const [locale, bundle] of Object.entries(BUNDLES))
|
|
211
|
+
expect({ status, locale, has: `inventory.status_${status}` in bundle }).toEqual({
|
|
212
|
+
status,
|
|
213
|
+
locale,
|
|
214
|
+
has: true,
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The counted messages, resolved the way the runtime resolves them.
|
|
221
|
+
*
|
|
222
|
+
* `t()` lives in `@kernhq/ui`, whose entry point pulls in Svelte components this package's test
|
|
223
|
+
* setup cannot transform. So these drive the *data* through the same `Intl.PluralRules` selection
|
|
224
|
+
* `selectPlural` performs, and `t()` itself is tested where it lives.
|
|
225
|
+
*/
|
|
226
|
+
describe('counted messages', () => {
|
|
227
|
+
const pick = (locale: Locale, key: string, count: number): string => {
|
|
228
|
+
const value = BUNDLES[locale][key as keyof (typeof BUNDLES)[Locale]] as
|
|
229
|
+
| string
|
|
230
|
+
| Partial<Record<Intl.LDMLPluralRule, string>>
|
|
231
|
+
if (typeof value === 'string') return value
|
|
232
|
+
const category = new Intl.PluralRules(locale).select(count)
|
|
233
|
+
const form = value[category] ?? value.other
|
|
234
|
+
if (form === undefined) throw new Error(`${locale} ${key} has no form for ${count}`)
|
|
235
|
+
return form.replace(/\{n\}/g, new Intl.NumberFormat(locale).format(count))
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
it('says "1 asset" and "2 assets" in English', () => {
|
|
239
|
+
expect(pick('en', 'inventory.count', 1)).toBe('1 asset')
|
|
240
|
+
expect(pick('en', 'inventory.count', 2)).toBe('2 assets')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('says "1 Gegenstand" and "2 Gegenstände" in German', () => {
|
|
244
|
+
expect(pick('de', 'inventory.count', 1)).toBe('1 Gegenstand')
|
|
245
|
+
expect(pick('de', 'inventory.count', 2)).toBe('2 Gegenstände')
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
it('follows Arabic numeral agreement rather than one-or-many', () => {
|
|
249
|
+
expect(pick('ar', 'inventory.count', 0)).toBe('لا أصول')
|
|
250
|
+
expect(pick('ar', 'inventory.count', 1)).toBe('أصل واحد')
|
|
251
|
+
expect(pick('ar', 'inventory.count', 2)).toBe('أصلان')
|
|
252
|
+
expect(pick('ar', 'inventory.count', 5)).toContain('أصول')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('does not inflect the noun after a numeral in Turkish or Persian', () => {
|
|
256
|
+
// "5 demirbaş", not "5 demirbaşlar" — the plural suffix is wrong once a number is present.
|
|
257
|
+
expect(pick('tr', 'inventory.count', 1).replace('1', '')).toBe(
|
|
258
|
+
pick('tr', 'inventory.count', 5).replace('5', ''),
|
|
259
|
+
)
|
|
260
|
+
expect(pick('fa', 'inventory.count', 1).replace('۱', '')).toBe(
|
|
261
|
+
pick('fa', 'inventory.count', 5).replace('۵', ''),
|
|
262
|
+
)
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('formats the number in the reader’s own digits', () => {
|
|
266
|
+
expect(pick('fa', 'inventory.count', 5)).toContain('۵')
|
|
267
|
+
expect(pick('en', 'inventory.count', 5)).toContain('5')
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* One word per idea, across the whole product and not just across this module.
|
|
273
|
+
*
|
|
274
|
+
* A module is read beside the shell, so a second Turkish word for un-archiving something is a
|
|
275
|
+
* product that says two things for one action. `ws_archive_hint` and `ws_archive_confirm_body` in
|
|
276
|
+
* `shell/messages/tr.json` both say **geri getir** about an archived workspace; this bundle said
|
|
277
|
+
* *geri yükle*, three lines away from "Daha fazla yükle" (load more) and "sayfayı yeniden yükleyin"
|
|
278
|
+
* (reload) — one verb doing three jobs in one panel.
|
|
279
|
+
*
|
|
280
|
+
* Pinned here because it cannot be checked against the shell: a module is built standalone, and
|
|
281
|
+
* `shell/messages/*.json` is not in this package. The word is the decision; the test is the memory.
|
|
282
|
+
*/
|
|
283
|
+
describe('the words Kern already uses', () => {
|
|
284
|
+
it('un-archives with the shell’s own Turkish verb, and never overloads “yükle”', () => {
|
|
285
|
+
expect(tr['inventory.restore']).toBe('Geri getir')
|
|
286
|
+
const overloaded = Object.entries(tr)
|
|
287
|
+
.filter(([, value]) => typeof value === 'string' && (value as string).includes('geri yükle'))
|
|
288
|
+
.map(([key]) => key)
|
|
289
|
+
expect(overloaded).toEqual([])
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
/** «إعادة التحميل» is what the shell calls Reload (`pwa_update_reload`), so a reload says that. */
|
|
293
|
+
it('reloads with the shell’s own words in Arabic', () => {
|
|
294
|
+
expect(ar['inventory.error_conflict']).toContain('أعد تحميل الصفحة')
|
|
295
|
+
})
|
|
296
|
+
})
|