@hestia-earth/ui-components 0.42.14 → 0.42.16

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.
@@ -0,0 +1,1324 @@
1
+ import { TermTermType, SchemaType, isTypeValid, isTypeBlankNode, isTypeNode, NodeType, EmissionMethodTier, SiteSiteType, CycleFunctionalUnit, SCHEMA_VERSION } from '@hestia-earth/schema';
2
+ import { keyToLabel, toDashCase, unique, toPrecision } from '@hestia-earth/utils';
3
+ import { validationsByMessage } from '@hestia-earth/data-validation';
4
+
5
+ /* eslint-disable */
6
+ // copied from https://github.com/plurals/pluralize but incompatible with angular 14 and webpack 6
7
+ // Rule storage - pluralize and singularize need to be run sequentially,
8
+ // while other rules can be optimized using an object for instant lookups.
9
+ const pluralRules = [];
10
+ const singularRules = [];
11
+ const uncountables = {};
12
+ const irregularPlurals = {};
13
+ const irregularSingles = {};
14
+ /**
15
+ * Sanitize a pluralization rule to a usable regular expression.
16
+ *
17
+ * @param rule
18
+ * @return
19
+ */
20
+ const sanitizeRule = rule => {
21
+ if (typeof rule === 'string') {
22
+ return new RegExp('^' + rule + '$', 'i');
23
+ }
24
+ return rule;
25
+ };
26
+ /**
27
+ * Pass in a word token to produce a function that can replicate the case on
28
+ * another word.
29
+ *
30
+ * @param word
31
+ * @param token
32
+ * @return
33
+ */
34
+ const restoreCase = (word, token) => {
35
+ // Tokens are an exact match.
36
+ if (word === token) {
37
+ return token;
38
+ }
39
+ // Lower cased words. E.g. "hello".
40
+ if (word === word.toLowerCase()) {
41
+ return token.toLowerCase();
42
+ }
43
+ // Upper cased words. E.g. "WHISKY".
44
+ if (word === word.toUpperCase()) {
45
+ return token.toUpperCase();
46
+ }
47
+ // Title cased words. E.g. "Title".
48
+ if (word[0] === word[0].toUpperCase()) {
49
+ return token.charAt(0).toUpperCase() + token.substr(1).toLowerCase();
50
+ }
51
+ // Lower cased words. E.g. "test".
52
+ return token.toLowerCase();
53
+ };
54
+ /**
55
+ * Interpolate a regexp string.
56
+ *
57
+ * @param str
58
+ * @param args
59
+ * @return
60
+ */
61
+ const interpolate = (str, args) => str.replace(/\$(\d{1,2})/g, (match, index) => args[index] || '');
62
+ /**
63
+ * Replace a word using a rule.
64
+ *
65
+ * @param word
66
+ * @param rule
67
+ * @return
68
+ */
69
+ const replace = (word, rule) => word.replace(rule[0], function (match, index) {
70
+ const result = interpolate(rule[1], arguments);
71
+ if (match === '') {
72
+ return restoreCase(word[index - 1], result);
73
+ }
74
+ return restoreCase(match, result);
75
+ });
76
+ /**
77
+ * Sanitize a word by passing in the word and sanitization rules.
78
+ *
79
+ * @param token
80
+ * @param word
81
+ * @param rules
82
+ * @return
83
+ */
84
+ const sanitizeWord = (token, word, rules) => {
85
+ // Empty string or doesn't need fixing.
86
+ if (!token.length || uncountables.hasOwnProperty(token)) {
87
+ return word;
88
+ }
89
+ let len = rules.length;
90
+ // Iterate over the sanitization rules and use the first one to match.
91
+ while (len--) {
92
+ const rule = rules[len];
93
+ if (rule[0].test(word)) {
94
+ return replace(word, rule);
95
+ }
96
+ }
97
+ return word;
98
+ };
99
+ /**
100
+ * Replace a word with the updated word.
101
+ *
102
+ * @param replaceMap
103
+ * @param keepMap
104
+ * @param rules
105
+ * @return
106
+ */
107
+ const replaceWord = (replaceMap, keepMap, rules) => (word = '') => {
108
+ // Get the correct token and case restoration functions.
109
+ const token = word.toLowerCase();
110
+ // Check against the keep object map.
111
+ if (keepMap.hasOwnProperty(token)) {
112
+ return restoreCase(word, token);
113
+ }
114
+ // Check against the replacement map for a direct word replacement.
115
+ if (replaceMap.hasOwnProperty(token)) {
116
+ return restoreCase(word, replaceMap[token]);
117
+ }
118
+ // Run all the rules against the word.
119
+ return sanitizeWord(token, word, rules);
120
+ };
121
+ /**
122
+ * Check if a word is part of the map.
123
+ */
124
+ const checkWord = (replaceMap, keepMap, rules) => (word = '') => {
125
+ const token = word.toLowerCase();
126
+ if (keepMap.hasOwnProperty(token)) {
127
+ return true;
128
+ }
129
+ if (replaceMap.hasOwnProperty(token)) {
130
+ return false;
131
+ }
132
+ return sanitizeWord(token, token, rules) === token;
133
+ };
134
+ /**
135
+ * Pluralize or singularize a word based on the passed in count.
136
+ *
137
+ * @param word The word to pluralize
138
+ * @param count How many of the word exist
139
+ * @param inclusive Whether to prefix with the number (e.g. 3 ducks)
140
+ * @return
141
+ */
142
+ const pluralize = (word, count, inclusive = false) => {
143
+ const pluralized = count === 1 ? pluralize.singular(word) : pluralize.plural(word);
144
+ return (inclusive ? count + ' ' : '') + pluralized;
145
+ };
146
+ /**
147
+ * Pluralize a word.
148
+ *
149
+ * @type {Function}
150
+ */
151
+ pluralize.plural = replaceWord(irregularSingles, irregularPlurals, pluralRules);
152
+ /**
153
+ * Check if a word is plural.
154
+ *
155
+ * @type {Function}
156
+ */
157
+ pluralize.isPlural = checkWord(irregularSingles, irregularPlurals, pluralRules);
158
+ /**
159
+ * Singularize a word.
160
+ *
161
+ * @type {Function}
162
+ */
163
+ pluralize.singular = replaceWord(irregularPlurals, irregularSingles, singularRules);
164
+ /**
165
+ * Check if a word is singular.
166
+ *
167
+ * @type {Function}
168
+ */
169
+ pluralize.isSingular = checkWord(irregularPlurals, irregularSingles, singularRules);
170
+ /**
171
+ * Add a pluralization rule to the collection.
172
+ *
173
+ * @param rule
174
+ * @param replacement
175
+ */
176
+ pluralize.addPluralRule = (rule, replacement) => {
177
+ pluralRules.push([sanitizeRule(rule), replacement]);
178
+ };
179
+ /**
180
+ * Add a singularization rule to the collection.
181
+ *
182
+ * @param rule
183
+ * @param replacement
184
+ */
185
+ pluralize.addSingularRule = (rule, replacement) => {
186
+ singularRules.push([sanitizeRule(rule), replacement]);
187
+ };
188
+ /**
189
+ * Add an uncountable word rule.
190
+ *
191
+ * @param word
192
+ */
193
+ pluralize.addUncountableRule = word => {
194
+ if (typeof word === 'string') {
195
+ uncountables[word.toLowerCase()] = true;
196
+ return;
197
+ }
198
+ // Set singular and plural references for the word.
199
+ pluralize.addPluralRule(word, '$0');
200
+ pluralize.addSingularRule(word, '$0');
201
+ };
202
+ /**
203
+ * Add an irregular word definition.
204
+ *
205
+ * @param single
206
+ * @param plural
207
+ */
208
+ pluralize.addIrregularRule = (single, plural) => {
209
+ plural = plural.toLowerCase();
210
+ single = single.toLowerCase();
211
+ irregularSingles[single] = plural;
212
+ irregularPlurals[plural] = single;
213
+ };
214
+ /**
215
+ * Irregular rules.
216
+ */
217
+ [
218
+ // Pronouns.
219
+ ['I', 'we'],
220
+ ['me', 'us'],
221
+ ['he', 'they'],
222
+ ['she', 'they'],
223
+ ['them', 'them'],
224
+ ['myself', 'ourselves'],
225
+ ['yourself', 'yourselves'],
226
+ ['itself', 'themselves'],
227
+ ['herself', 'themselves'],
228
+ ['himself', 'themselves'],
229
+ ['themself', 'themselves'],
230
+ ['is', 'are'],
231
+ ['was', 'were'],
232
+ ['has', 'have'],
233
+ ['this', 'these'],
234
+ ['that', 'those'],
235
+ // Words ending in with a consonant and `o`.
236
+ ['echo', 'echoes'],
237
+ ['dingo', 'dingoes'],
238
+ ['volcano', 'volcanoes'],
239
+ ['tornado', 'tornadoes'],
240
+ ['torpedo', 'torpedoes'],
241
+ // Ends with `us`.
242
+ ['genus', 'genera'],
243
+ ['viscus', 'viscera'],
244
+ // Ends with `ma`.
245
+ ['stigma', 'stigmata'],
246
+ ['stoma', 'stomata'],
247
+ ['dogma', 'dogmata'],
248
+ ['lemma', 'lemmata'],
249
+ ['schema', 'schemata'],
250
+ ['anathema', 'anathemata'],
251
+ // Other irregular rules.
252
+ ['ox', 'oxen'],
253
+ ['axe', 'axes'],
254
+ ['die', 'dice'],
255
+ ['yes', 'yeses'],
256
+ ['foot', 'feet'],
257
+ ['eave', 'eaves'],
258
+ ['goose', 'geese'],
259
+ ['tooth', 'teeth'],
260
+ ['quiz', 'quizzes'],
261
+ ['human', 'humans'],
262
+ ['proof', 'proofs'],
263
+ ['carve', 'carves'],
264
+ ['valve', 'valves'],
265
+ ['looey', 'looies'],
266
+ ['thief', 'thieves'],
267
+ ['groove', 'grooves'],
268
+ ['pickaxe', 'pickaxes'],
269
+ ['passerby', 'passersby']
270
+ ].forEach(rule => pluralize.addIrregularRule(rule[0], rule[1]));
271
+ /**
272
+ * Pluralization rules.
273
+ */
274
+ [
275
+ [/s?$/i, 's'],
276
+ [/[^\u0000-\u007F]$/i, '$0'],
277
+ [/([^aeiou]ese)$/i, '$1'],
278
+ [/(ax|test)is$/i, '$1es'],
279
+ [/(alias|[^aou]us|t[lm]as|gas|ris)$/i, '$1es'],
280
+ [/(e[mn]u)s?$/i, '$1s'],
281
+ [/([^l]ias|[aeiou]las|[ejzr]as|[iu]am)$/i, '$1'],
282
+ [/(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, '$1i'],
283
+ [/(alumn|alg|vertebr)(?:a|ae)$/i, '$1ae'],
284
+ [/(seraph|cherub)(?:im)?$/i, '$1im'],
285
+ [/(her|at|gr)o$/i, '$1oes'],
286
+ [
287
+ /(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|automat|quor)(?:a|um)$/i,
288
+ '$1a'
289
+ ],
290
+ [/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)(?:a|on)$/i, '$1a'],
291
+ [/sis$/i, 'ses'],
292
+ [/(?:(kni|wi|li)fe|(ar|l|ea|eo|oa|hoo)f)$/i, '$1$2ves'],
293
+ [/([^aeiouy]|qu)y$/i, '$1ies'],
294
+ [/([^ch][ieo][ln])ey$/i, '$1ies'],
295
+ [/(x|ch|ss|sh|zz)$/i, '$1es'],
296
+ [/(matr|cod|mur|sil|vert|ind|append)(?:ix|ex)$/i, '$1ices'],
297
+ [/\b((?:tit)?m|l)(?:ice|ouse)$/i, '$1ice'],
298
+ [/(pe)(?:rson|ople)$/i, '$1ople'],
299
+ [/(child)(?:ren)?$/i, '$1ren'],
300
+ [/eaux$/i, '$0'],
301
+ [/m[ae]n$/i, 'men'],
302
+ ['thou', 'you']
303
+ ].forEach(rule => pluralize.addPluralRule(rule[0], rule[1]));
304
+ /**
305
+ * Singularization rules.
306
+ */
307
+ [
308
+ [/s$/i, ''],
309
+ [/(ss)$/i, '$1'],
310
+ [/(wi|kni|(?:after|half|high|low|mid|non|night|[^\w]|^)li)ves$/i, '$1fe'],
311
+ [/(ar|(?:wo|[ae])l|[eo][ao])ves$/i, '$1f'],
312
+ [/ies$/i, 'y'],
313
+ [/\b([pl]|zomb|(?:neck|cross)?t|coll|faer|food|gen|goon|group|lass|talk|goal|cut)ies$/i, '$1ie'],
314
+ [/\b(mon|smil)ies$/i, '$1ey'],
315
+ [/\b((?:tit)?m|l)ice$/i, '$1ouse'],
316
+ [/(seraph|cherub)im$/i, '$1'],
317
+ [/(x|ch|ss|sh|zz|tto|go|cho|alias|[^aou]us|t[lm]as|gas|(?:her|at|gr)o|[aeiou]ris)(?:es)?$/i, '$1'],
318
+ [/(analy|diagno|parenthe|progno|synop|the|empha|cri|ne)(?:sis|ses)$/i, '$1sis'],
319
+ [/(movie|twelve|abuse|e[mn]u)s$/i, '$1'],
320
+ [/(test)(?:is|es)$/i, '$1is'],
321
+ [/(alumn|syllab|vir|radi|nucle|fung|cact|stimul|termin|bacill|foc|uter|loc|strat)(?:us|i)$/i, '$1us'],
322
+ [/(agend|addend|millenni|dat|extrem|bacteri|desiderat|strat|candelabr|errat|ov|symposi|curricul|quor)a$/i, '$1um'],
323
+ [/(apheli|hyperbat|periheli|asyndet|noumen|phenomen|criteri|organ|prolegomen|hedr|automat)a$/i, '$1on'],
324
+ [/(alumn|alg|vertebr)ae$/i, '$1a'],
325
+ [/(cod|mur|sil|vert|ind)ices$/i, '$1ex'],
326
+ [/(matr|append)ices$/i, '$1ix'],
327
+ [/(pe)(rson|ople)$/i, '$1rson'],
328
+ [/(child)ren$/i, '$1'],
329
+ [/(eau)x?$/i, '$1'],
330
+ [/men$/i, 'man']
331
+ ].forEach(rule => pluralize.addSingularRule(rule[0], rule[1]));
332
+ /**
333
+ * Uncountable rules.
334
+ */
335
+ [
336
+ // Singular words with no plurals.
337
+ 'adulthood',
338
+ 'advice',
339
+ 'agenda',
340
+ 'aid',
341
+ 'aircraft',
342
+ 'alcohol',
343
+ 'ammo',
344
+ 'analytics',
345
+ 'anime',
346
+ 'athletics',
347
+ 'audio',
348
+ 'bison',
349
+ 'blood',
350
+ 'bream',
351
+ 'buffalo',
352
+ 'butter',
353
+ 'carp',
354
+ 'cash',
355
+ 'chassis',
356
+ 'chess',
357
+ 'clothing',
358
+ 'cod',
359
+ 'commerce',
360
+ 'cooperation',
361
+ 'corps',
362
+ 'debris',
363
+ 'diabetes',
364
+ 'digestion',
365
+ 'elk',
366
+ 'energy',
367
+ 'equipment',
368
+ 'excretion',
369
+ 'expertise',
370
+ 'firmware',
371
+ 'flounder',
372
+ 'fun',
373
+ 'gallows',
374
+ 'garbage',
375
+ 'graffiti',
376
+ 'hardware',
377
+ 'headquarters',
378
+ 'health',
379
+ 'herpes',
380
+ 'highjinks',
381
+ 'homework',
382
+ 'housework',
383
+ 'information',
384
+ 'jeans',
385
+ 'justice',
386
+ 'kudos',
387
+ 'labour',
388
+ 'literature',
389
+ 'machinery',
390
+ 'mackerel',
391
+ 'mail',
392
+ 'media',
393
+ 'mews',
394
+ 'moose',
395
+ 'music',
396
+ 'mud',
397
+ 'manga',
398
+ 'news',
399
+ 'only',
400
+ 'personnel',
401
+ 'pike',
402
+ 'plankton',
403
+ 'pliers',
404
+ 'police',
405
+ 'pollution',
406
+ 'premises',
407
+ 'rain',
408
+ 'research',
409
+ 'rice',
410
+ 'salmon',
411
+ 'scissors',
412
+ 'series',
413
+ 'sewage',
414
+ 'shambles',
415
+ 'shrimp',
416
+ 'software',
417
+ 'species',
418
+ 'staff',
419
+ 'swine',
420
+ 'tennis',
421
+ 'traffic',
422
+ 'transportation',
423
+ 'trout',
424
+ 'tuna',
425
+ 'wealth',
426
+ 'welfare',
427
+ 'whiting',
428
+ 'wildebeest',
429
+ 'wildlife',
430
+ 'you',
431
+ /pok[eé]mon$/i,
432
+ // Regexes.
433
+ /[^aeiou]ese$/i, // "chinese", "japanese"
434
+ /deer$/i, // "deer", "reindeer"
435
+ /fish$/i, // "fish", "blowfish", "angelfish"
436
+ /measles$/i,
437
+ /o[iu]s$/i, // "carnivorous"
438
+ /pox$/i, // "chickpox", "smallpox"
439
+ /sheep$/i
440
+ ].forEach(pluralize.addUncountableRule);
441
+
442
+ /**
443
+ * Browser-free copy of `termTypeLabel` from `../terms/terms.model`, kept here so this
444
+ * entry point stays self-contained.
445
+ */
446
+ const termTypeToLabel = {
447
+ [TermTermType.methodEmissionResourceUse]: 'Method (Emissions)',
448
+ [TermTermType.methodMeasurement]: 'Method (Measurement)',
449
+ [TermTermType.pesticideAI]: 'Pesticide Active Ingredient',
450
+ [TermTermType.standardsLabels]: 'Standards & Labels',
451
+ [TermTermType.usdaSoilType]: 'USDA Soil Type'
452
+ };
453
+ const termKeyToLabel = {
454
+ pubchem: 'PubChem',
455
+ casNumber: 'CAS Number',
456
+ agrovoc: 'AGROVOC'
457
+ };
458
+ const termTypeLabel = (value = 'N/A') => termTypeToLabel[value] || termKeyToLabel[value] || keyToLabel(value);
459
+
460
+ /**
461
+ * Minimal, browser-free subset of `../common/utils` needed to build issue/report links.
462
+ * Duplicated here to keep this entry point self-contained and free of `window` access.
463
+ */
464
+ const gitHome = 'https://gitlab.com/hestia-earth';
465
+ const contactUsEmail = 'community@hestia.earth';
466
+ var Repository;
467
+ (function (Repository) {
468
+ Repository["glossary"] = "hestia-glossary";
469
+ Repository["models"] = "hestia-engine-models";
470
+ Repository["aggregation"] = "hestia-aggregation-engine";
471
+ Repository["community"] = "hestia-community-edition";
472
+ Repository["schema"] = "hestia-schema";
473
+ Repository["frontend"] = "hestia-front-end";
474
+ })(Repository || (Repository = {}));
475
+ var Template;
476
+ (function (Template) {
477
+ Template["bug"] = "bug";
478
+ Template["feature"] = "feature";
479
+ })(Template || (Template = {}));
480
+ const reportIssueUrl = (repository, template, issueParams = {}) => [
481
+ `${gitHome}/${repository}/-/issues/new`,
482
+ [
483
+ template ? `issuable_template=${template}` : '',
484
+ ...Object.entries(issueParams).map(([key, value]) => key && value ? `issue[${key}]=${encodeURIComponent(value)}` : null)
485
+ ]
486
+ .filter(Boolean)
487
+ .join('&')
488
+ ]
489
+ .filter(Boolean)
490
+ .join('?');
491
+
492
+ /* eslint-disable complexity */
493
+ /* eslint-disable max-len */
494
+ /* eslint-disable no-useless-escape */
495
+ const guidePageId = (nodeType, errorMessage) => {
496
+ const error = validationsByMessage?.[errorMessage]?.[0]?.function;
497
+ return error ? [toDashCase(nodeType), error].filter(Boolean).join('#') : null;
498
+ };
499
+ const mapErrorMessage = 'does not contain latitude and longitude';
500
+ const allowedDataPathsLabels = Object.values(SchemaType);
501
+ const idPatternError = (chars) => `You can only use the following characters for this field: ${chars.join(', ')}`;
502
+ const noTillage = { id: 'noTillage', name: 'No tillage' };
503
+ const pastureGrass = { id: 'pastureGrass', name: 'Pasture grass' };
504
+ const isMigrationError = ({ params }) => params?.current && 'expected' in params;
505
+ /**
506
+ * Builds the error formatting functions for a given presentation (`renderer`) and link
507
+ * configuration (`urls`). The error catalog below is the single source of truth; the
508
+ * renderer alone decides whether the output is HTML, plain text, etc.
509
+ */
510
+ const createErrorFormatter = ({ renderer: r, urls }) => {
511
+ const code = (text) => r.code(text);
512
+ const externalLink = (href, text) => r.link(href, text);
513
+ const glossaryLink = (text) => r.link(urls.glossaryBaseUrl(), text);
514
+ const externalNodeLink = ({ '@type': type, '@id': id, name }) => (type && id ? r.link(`/${type.toLowerCase()}/${id}`, name || id) : null);
515
+ const schemaLink = (type, title = type) => r.link(`${urls.schemaBaseUrl()}/${type}`, title);
516
+ const contactUsLink = (text = 'contact us') => r.link(`mailto:${contactUsEmail}`, text, false);
517
+ const reportIssueLink = (repository, template, text = 'here') => r.link(reportIssueUrl(repository, template), text);
518
+ const guideLink = (guidePath, title) => r.link(`${urls.baseUrl()}/guide/${guidePath}`, title);
519
+ const glossaryTypeLink = (type, text = termTypeLabel(type)) => r.link(`${urls.glossaryBaseUrl()}?termType=${type}`, text);
520
+ const termLink = ({ id, name }) => r.link(`${urls.baseUrl()}/term/${id}`, name || id);
521
+ const listAsCode = (values) => (values ?? []).map(value => r.code(value)).join(', ');
522
+ const joinValues = (values) => (values || []).filter(Boolean).map(value => r.code(value)).join(r.inlineSeparator);
523
+ const formatList = (values, style = 'disc') => r.list(values, style);
524
+ const threshold = (value) => r.code(`${value * 100}%`);
525
+ const dateFormatMessage = `Should follow the ISO 8601 date format, e.g. ${r.code(2000)}, or ${r.code('2000-12')}, or ${r.code('2000-12-30')}, and in some cases an optional time. Please refer to the schema for more details.`;
526
+ const idNotFoundDefaultMessage = [
527
+ `If you are using the ${r.code('name')} of the Term, make sure to use ${r.code('term.name')} column;`,
528
+ `if you are using the ${r.code('@id')} of the Term, use the ${r.code('term.@id')} column instead.`
529
+ ].join(' ');
530
+ const parseDataPath = (dataPath = '') => {
531
+ const [_, ...paths] = dataPath
532
+ .replace(/[\[\]\'\'\"\"]/g, '')
533
+ .replace(/(\w)(\@)/g, '$1.@')
534
+ .split('.');
535
+ return paths.map(path => {
536
+ const label = path.replace(/[\d\@]+/g, '');
537
+ const type = pluralize(keyToLabel(label), 1);
538
+ return {
539
+ nodeType: isTypeNode(type) ? type : null,
540
+ schemaType: isTypeBlankNode(type) ? type : null,
541
+ path: path.replace(/(\d+)/g, '[$1]'),
542
+ label: isTypeValid({ type }) ? type : r.code(pluralize(label, 1))
543
+ };
544
+ });
545
+ };
546
+ const dataPathLabel = (dataPath = '') => parseDataPath(dataPath)
547
+ .filter(({ label }) => allowedDataPathsLabels.includes(label))
548
+ .pop()?.label;
549
+ const migrationErrorMessage = ({ params }) => params?.expected
550
+ ? `The Term ${r.code(params.current)} was updated, please use this Term instead: ${r.code(params.expected)}.`
551
+ : `The Term ${r.code(params.current)} was deleted. Please search the ${glossaryLink('Glossary')} for a new Term.`;
552
+ const idNotFoundError = (_error, _errorCount, allErrors) => {
553
+ const migrationErrors = (allErrors || []).filter(isMigrationError);
554
+ const noMigrationErrors = (allErrors || []).filter(error => !isMigrationError(error));
555
+ return [
556
+ r.paragraph('Does not exist in HESTIA.'),
557
+ noMigrationErrors.length ? r.paragraph(idNotFoundDefaultMessage) : '',
558
+ unique(migrationErrors.map(migrationErrorMessage)).join(r.lineBreak)
559
+ ]
560
+ .filter(Boolean)
561
+ .join('');
562
+ };
563
+ const methodModelMappingError = ({ params }, errorCount) => `${errorCount === 1
564
+ ? `${params?.allowedValues.length
565
+ ? `can only be used with one of these methods: ${listAsCode(params?.allowedValues)}`
566
+ : `does not currently have any method allowed. Please ${contactUsLink()} to change this.`}.`
567
+ : `Some Terms are not allowed with the method you selected.`}
568
+ You can find the list of allowed methods for each Term by clicking on them.`;
569
+ const formatRule = (rule, path = '', negation = false) => rule?.enum?.length
570
+ ? `${r.code(path)} with of the following values: ${joinValues(rule.enum)}.`
571
+ : rule.const
572
+ ? `${r.code(path)} = ${r.code(rule.const)}`
573
+ : rule.properties
574
+ ? Object.entries(rule.properties).map(([key, value]) => formatRule(value, [path, key].filter(Boolean).join('.')))
575
+ : rule.items
576
+ ? formatRule(rule.items, path)
577
+ : rule.not
578
+ ? formatRule(rule.not, path, true)
579
+ : rule.anyOf
580
+ ? `${r.underline(negation ? 'none' : 'one')} of the following must be valid: ${formatList(rule.anyOf.map(nested => formatRule(nested)))}`
581
+ : '';
582
+ const patternErrorKey = (pattern) => `should match pattern "${pattern}"`;
583
+ // set message as empty to not display it
584
+ const customErrorMessage = {
585
+ 'this field is required': ({ dataPath }, _errorCount, allErrors) => {
586
+ const parts = parseDataPath(dataPath);
587
+ const key = parts[parts.length - 1];
588
+ const type = parts[parts.length - 2];
589
+ return allErrors?.length
590
+ ? `You are missing the following required field: ${key.label} (for ${type.label}).`
591
+ : 'This field is required.';
592
+ },
593
+ 'should not be empty': ({ params }) => [
594
+ `Empty ${params.type} are not allowed in HESTIA.`,
595
+ params?.type === NodeType.ImpactAssessment
596
+ ? `Please, either link your ImpactAssessment to an existing Cycle using the ${code('cycle.id')},
597
+ or add one or more ${schemaLink('ImpactAssessment#emissionsResourceUse', 'emissionsResourceUse')},
598
+ ${schemaLink('ImpactAssessment#impacts', 'impacts')},
599
+ or ${schemaLink('ImpactAssessment#endpoints', 'endpoints')}.`
600
+ : ''
601
+ ].join('\n'),
602
+ "should have required property '@type'": ({ dataPath }) => {
603
+ // make sure schemaType is the last path
604
+ const { schemaType } = parseDataPath(dataPath).pop() ?? {};
605
+ // ignore .inputs as Term, find a better solution by parsing the dataPath correctly
606
+ // @see https://gitlab.com/hestia-earth/hestia-ui-components/-/issues/399
607
+ const isInputAsTerm = dataPath.match(/(emissions|emissionsResourceUse)\[[\d]+\].inputs[\[\d]+\]$/g);
608
+ return schemaType && !isInputAsTerm ? `Please add ${code(`"@type" = "${schemaType}"`)} to this blank node.` : '';
609
+ },
610
+ "should have required property '@id'": idNotFoundError,
611
+ "should have required property 'id'": idNotFoundError,
612
+ 'should match exactly one schema in oneOf': () => '',
613
+ 'should match some schema in anyOf': () => '',
614
+ 'should match "then" schema': () => '',
615
+ 'should be null': () => '',
616
+ 'should NOT have additional properties': ({ params }, errorCount) => errorCount === 1
617
+ ? `The following field does not exist: ${params?.additionalProperty} or is not allowed in this case.`
618
+ : `Should not have additional properties.`,
619
+ 'should NOT be valid': ({ schema }) => schema?.required?.includes('areaPercent')
620
+ ? `If the term units is already in ${code('% area')}, just use ${code('value')}. Dont use ${code('areaPercent')}.`
621
+ : schema?.required?.includes('transformation')
622
+ ? 'A transformation can not be added here.'
623
+ : schema?.required?.includes('fromCycle')
624
+ ? `This field can only be added to the ${schemaLink('Transformation#inputs', 'inputs of a Transformation')}.`
625
+ : schema?.required?.includes('otherSites')
626
+ ? `Please only specify a single ${schemaLink('Cycle#site', 'site')} when the functional unit is ${code(CycleFunctionalUnit['1 ha'])}.`
627
+ : // termType restriction on the item
628
+ schema?.properties?.term?.properties?.termType?.enum?.length
629
+ ? `This ${code('termType')} is not allowed here.`
630
+ : schema?.required
631
+ ? `The following ${pluralize('field', schema.required.length)} should not be used: ${listAsCode(schema.required)}`
632
+ : 'Should not be used in this case.',
633
+ [patternErrorKey('^[0-9]{4}(-01)?(-01)?$')]: () => 'should be the first month/day of the year',
634
+ [patternErrorKey('^\\d{13}$')]: () => 'should be composed of 13 numbers',
635
+ 'not a valid date': () => dateFormatMessage,
636
+ [patternErrorKey('^[0-9]{4}(-[0-9]{2})?(-[0-9]{2})?$')]: () => dateFormatMessage,
637
+ [patternErrorKey('^([0-9]{4}|-)(-[0-9]{2})?(-[0-9]{2})?$')]: () => dateFormatMessage,
638
+ [patternErrorKey('^([0-9]{4}|-)(-[0-9]{2})?(-[0-9]{2})?([T][0-2][0-9]:[0-5][0-9]:[0-5][0-9]((+|-)[0-1][0-9]:[0-5][0-9])?)?$')]: () => dateFormatMessage,
639
+ [patternErrorKey('^10.(d)+/(S)+$')]: () => `The DOI should not include the domain name, e.g. ${code('10.1000/182')} instead of ${code('https://doi.org/10.1000/182')}`,
640
+ [patternErrorKey('^[\\w\\-\\.]+$')]: () => idPatternError(['letters', 'numbers', 'underscores', 'dashes', 'dots (.)']),
641
+ [patternErrorKey('^[\\w\\-\\.\\s]+$')]: () => idPatternError(['letters', 'numbers', 'underscores', 'dashes', 'dots (.)', 'spaces']),
642
+ [patternErrorKey('^[\\w\\-\\.\\s\\(\\)]+$')]: () => idPatternError(['letters', 'numbers', 'underscores', 'dashes', 'dots (.)', 'spaces', 'parentheses']),
643
+ [patternErrorKey('^[\\w\\-\\.\\s\\(\\),]+$')]: () => idPatternError(['letters', 'numbers', 'underscores', 'dashes', 'dots (.)', 'spaces', 'parentheses', 'commas']),
644
+ [patternErrorKey('^[\\w\\u00C0-\\u017F\\-\\.\\s\\(\\),]+$')]: () => idPatternError(['letters', 'numbers', 'underscores', 'dashes', 'dots (.)', 'spaces', 'parentheses', 'commas']),
645
+ [patternErrorKey('^[\\w\\u00C0-\\u017F\\-\\.\\s\\(\\),+]+$')]: () => idPatternError([
646
+ 'letters',
647
+ 'numbers',
648
+ 'underscores',
649
+ 'dashes',
650
+ 'dots (.)',
651
+ 'spaces',
652
+ 'parentheses',
653
+ 'commas',
654
+ 'plus'
655
+ ]),
656
+ 'should have the same length as endDate': () => `The startDate must be in the same date format as ${code('endDate')}: YYYY-MM-DD, YYYY-MM, or YYYY.`,
657
+ 'node not found': (error, _errorCount, allErrors) => `${allErrors?.length
658
+ ? `The following Nodes do not exist on the platform:
659
+ ${formatList(unique(allErrors.map(({ params }) => `${params?.node?.['@type']} with ${code(`@id=${params?.node?.['@id']}`)}`)))}`
660
+ : `The ${error.params?.node?.['@type']} with ${code(`@id=${error.params?.node?.['@id']}`)} does not exist on the platform.`} If you are trying to link to nodes included in this upload, you must use ${code('id')} field instead. Otherwise, please check the ids are correct and try again.`,
661
+ 'should be linked to an existing node': (error, _errorCount, allErrors) => allErrors?.length
662
+ ? `Your upload does not contain the following Nodes:
663
+ ${formatList(unique(allErrors.map(({ params }) => `${params?.node?.type} with ${code(`id=${params?.node?.id}`)}`)))}
664
+ If you are trying to link to an existing Node on the HESTIA platform, you must use ${code('@id')} field instead.
665
+ Otherwise you must include a full ${unique(allErrors.map(({ params }) => `${params?.node?.type} with id ${code(params?.node?.id)}.`)).join(', ')}`
666
+ : `Your upload does not contain a ${error.params?.node?.type} with ${code(`id=${error.params?.node?.id}`)}.
667
+ If you are trying to link to an existing Node on the HESTIA platform, you must use ${code('@id')} field instead.
668
+ Otherwise you must include a full ${error.params?.node?.type} with id ${code(error.params?.node?.id)}.`,
669
+ 'must be within the country': () => `The country provided does not contain the region provided. Please check
670
+ the country for errors or the region for errors, and reference the ${glossaryLink('Glossary of Terms')}.`,
671
+ [mapErrorMessage]: ({ dataPath, params }, errorCount) => errorCount === 1
672
+ ? `The longitude and latitude provided are${params?.distance ? ` ${params?.distance}kms` : ''} outside of the ${dataPath.substring(1)} provided.
673
+ Please check the longitude and latitude for errors or check if the wrong ${dataPath.substring(1)} from the ${glossaryTypeLink(TermTermType.region, 'Glossary')} was used.
674
+ ${params?.expected
675
+ ? `Hint: the coordinates appear to be located in the following ${dataPath.substring(1)}: ${termLink({
676
+ id: params.expected
677
+ })}`
678
+ : ''}`
679
+ : `The region provided does not contain the longitude and latitude provided.
680
+ Please check the longitude and latitude for errors or the region for error, and reference the ${glossaryTypeLink(TermTermType.region, 'Glossary of Terms')}.`,
681
+ 'sum not equal to 100% for sandContent, siltContent, clayContent': () => `The sum of Sand, Silt, and Clay content should equal 100% for each soil depth interval.`,
682
+ 'is outside the allowed range': ({ params }, errorCount) => `${errorCount === 1
683
+ ? params?.term
684
+ ? `The value for ${code(params?.term['@id'] || params?.term.name)} is outside the range established for the soil texture you provided.
685
+ For this soil texture, the maximum ${code(params?.term['@id'] || params?.term.name)} is ${params?.range?.max} and the minimum is ${params?.range?.min}.`
686
+ : `Does not match the possible ranges of sand, silt and clay content.`
687
+ : `Each soil texture has a maximum and minimum value for sand, silt, and clay content.
688
+ At least one measurement of sand, silt, or clay content does not match the possible range for the specified soil texture.`}
689
+ ${errorCount === 1
690
+ ? `Please check the measurement, its depth intervals, and the dates of the measurement or update the soil texture if needed.`
691
+ : `Please check your measurements, their depth intervals, and the dates of the measurements or update the soil textures if needed.`}`,
692
+ 'should be equal to one of the allowed values': ({ params, dataPath, schemaPath }, count, errors) => {
693
+ const paths = parseDataPath(dataPath);
694
+ const values = paths.length >= 2 ? pluralize(paths[paths.length - 1].path, params?.allowedValues?.length || 0) : 'values';
695
+ // find the matching conditional `if`
696
+ const schemaPathMatch = schemaPath?.split('/').slice(0, 3).join('/');
697
+ const ifError = schemaPathMatch
698
+ ? errors.find(error => [
699
+ error.keyword === 'if',
700
+ error.schemaPath !== schemaPath,
701
+ error.schemaPath?.startsWith(schemaPathMatch)
702
+ ].every(Boolean))
703
+ : null;
704
+ return [
705
+ dataPath.endsWith('term.termType')
706
+ ? [
707
+ `For ${pluralize(paths[paths.length - 3].label, 0)}, we only allow terms from the following glossaries: ${listAsCode(params?.allowedValues)}.`,
708
+ ifError
709
+ ? ''
710
+ : 'Please either change the Term you are using and ensure it is from one of the glossaries listed above, or follow the schema to identify where this Term can be added.'
711
+ ]
712
+ .filter(Boolean)
713
+ .join(' ')
714
+ : `Only the following ${values} are permitted: ${listAsCode(params?.allowedValues)}.`,
715
+ ifError?.schema.properties
716
+ ? `This validation applies when the following conditions are met:${formatList(Object.entries(ifError.schema.properties).map(([key, value]) => formatRule(value, key)))}`
717
+ : ''
718
+ ]
719
+ .filter(Boolean)
720
+ .join(' ');
721
+ },
722
+ 'should be equal to constant': ({ params }) => `Must be: ${code(params?.allowedValue)}`,
723
+ 'can only contain one primary item': ({ dataPath }) => `Only 1 primary ${parseDataPath(dataPath).pop().label} allowed.`,
724
+ 'is not allowed for this emission': methodModelMappingError,
725
+ 'is not allowed for this characterisedIndicator': methodModelMappingError,
726
+ 'is not allowed for this resourceUse': methodModelMappingError,
727
+ 'should contain a valid item': ({ dataPath, schema }) => {
728
+ const paths = parseDataPath(dataPath);
729
+ return paths.length >= 2
730
+ ? `The ${paths[paths.length - 1].path} related to this ${pluralize(paths[paths.length - 2].label, 1)} are invalid`
731
+ : `The ${paths[0].path} are invalid because they do not contain a blank node with:${formatList(Object.entries(schema?.properties).map(([key, value]) => formatRule(value, key)))}`;
732
+ },
733
+ 'may be between 0 and 100': () => 'Percentages should be between 0 and 100, not 0 and 1. This may be an error.',
734
+ 'may not all be set to false': () => `Every value in the data completeness assessment is ${code(false)}. You may have forgotten to fill it in.
735
+ For information on how to fill it in, please see the ${schemaLink('Cycle#completeness', 'schema')}.`,
736
+ 'may not be empty': () => 'if this value signifies no data, HESTIA only accepts "-" or empty for no data',
737
+ 'may not be 0': ({ dataPath }, errorCount) => `Adding a value ${errorCount === 1 ? ` to every ${dataPathLabel(dataPath)} ` : ' '} is highly recommended.
738
+ If the amount produced is zero, we recommend setting value to 0.`,
739
+ 'longFallowDuration must be lower than 5 years': () => `Your long fallow duration is greater than five years.
740
+ Fallow is defined by FAOSTAT as land left uncultivated for between 1 and 5 years,
741
+ and this definition is used for HESTIA also.`,
742
+ 'should be more than the cropping duration': ({ params }) => `The ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} is shorter than the country's minimum cropping duration for rice (${code(params?.expected)}).
743
+ This might be an error and result in the underestimation of the ${code('CH4, to air, flooded rice')} emissions.
744
+ Please, double-check that the ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} is correctly set.
745
+ `,
746
+ 'croppingDuration must be between min and max': ({ params }) => `The ${code('croppingDuration')} should be within the country's minimum (${params.min}) and maximum (${params.max}) cropping duration for rice.
747
+ This might be an error and result in the overestimation of ${code('CH4, to air, flooded rice')} emissions.
748
+ Please, double-check that the ${code('croppingDuration')} is correctly set.`,
749
+ 'should be within percentage of default value': ({ params }, errorCount) => errorCount === 1
750
+ ? `This value is ±${threshold(params?.threshold)} different to our default value which is ${params?.default}.
751
+ Please check your uploaded data as this may be an error.`
752
+ : `This value is substantially different than our default value.
753
+ Please check your uploaded data as this may be an error.`,
754
+ 'the value provided is not consistent with the model result': ({ params }, errorCount) => `The expected result ${errorCount === 1
755
+ ? `for ${code(params?.term.name)} ${params?.model?.name ? `using ${code(params.model.name)} ` : ''}`
756
+ : ''}
757
+ from the data provided in the upload is ${errorCount === 1
758
+ ? `${code(toPrecision(params?.expected, 3))}, but the value in the upload is ${code(toPrecision(params?.current, 3))}.
759
+ ${params?.threshold ? `Our threshold is ±${threshold(params?.threshold)}.` : ''}
760
+ Please either:
761
+ ${formatList([
762
+ 'Check the ${params?.term.termType} value you provided;',
763
+ 'Check the data you provided which is the input into this model;',
764
+ 'Carefully read the documentation to understand if we used a default property as part of the calculations.'
765
+ ], 'decimal')}`
766
+ : 'not consistent with the model result.'}`,
767
+ 'the measurement provided might be in error': ({ params }, errorCount) => `The expected value for ${code(params?.term.name)}
768
+ ${params?.model?.name ? `using ${code(params.model.name)} ` : ''}
769
+ from the data provided in the upload is ${errorCount === 1
770
+ ? `${code(toPrecision(params?.expected, 3))}, but the value in the upload is ${code(toPrecision(params?.current, 3))}.
771
+ ${params?.threshold ? `Our threshold is ±${threshold(params?.threshold)}.` : ''}`
772
+ : `not consistent with the model result.`}`,
773
+ 'must be equal to previous product multiplied by the share': () => `Products from a transformation which are an Input in to the next transformation must follow the following rule:
774
+ ${code('previous.product.value * current.transformedShare / 100 == current.input.value')}`,
775
+ 'at least one Input must be a Product of the Cycle': () => `A Transformation converts a Product from a Cycle into another Product.
776
+ Therefore, at least one Input into the Transformation must be a Product of the Cycle.`,
777
+ 'must have only one entry with the same term.termType = excretaManagement': () => `There can only be one Practice of type excretaManagement in a Cycle.
778
+ To represent multiple excreta management systems either use multiple Cycles and link them together,
779
+ or use Transformations within a Cycle and link those together.`,
780
+ 'must be included as an Input': ({ params }, errorCount) => `The excreta Products are not of the same type as the excreta Inputs.
781
+ ${errorCount === 1
782
+ ? `E.g., you cannot get ${code(params?.term?.name)} from ${(params?.expected || [])
783
+ .map(v => code(keyToLabel(v)))
784
+ .join(' or ')}.`
785
+ : ''}
786
+ Please check the Inputs and Products of the Transformation.`,
787
+ 'must add the linked inputs to the cycle': ({ params }, errorCount) => `${errorCount === 1
788
+ ? `You have stated that ${code(params?.term?.name)} was created by the following Inputs ${params?.expected
789
+ ?.map(e => code(e.name))
790
+ .join(' , ')}`
791
+ : 'You have stated some Emissions were created by some Inputs'}.
792
+ However, these Inputs do not exist in the Cycle.
793
+ Please add these Inputs, even if you do not have a value for them.`,
794
+ 'must add the linked transformations to the cycle': ({ level, params }, errorCount) => `${errorCount === 1
795
+ ? `You have specified that${code(params?.term?.name)} was created during a Transformation with the Term name ${code(params?.expected?.name)}`
796
+ : 'You have stated some Emissions were created by some Transformations'}.
797
+ However, we cannot find a Transformation within the Cycle with this Term name.
798
+ ${level === 'error'
799
+ ? 'Please check the Terms used in the Emissions and/or the Transformations.'
800
+ : 'You may want to add this Transformation.'}`,
801
+ 'every node should be unique': ({ params }) => `This Node is duplicated. Every Node should be unique.
802
+ Uniqueness is determined by the following fields: ${listAsCode(params?.keys)}`,
803
+ 'every item in the list should be unique': ({ params }) => `This blank node is duplicated${params?.duplicatedIndexes?.length
804
+ ? ` with the blank ${pluralize('node', params.duplicatedIndexes.length)} with numbered: ${params.duplicatedIndexes.join(', ')}`
805
+ : ''}. Every blank node should be unique.
806
+ Uniqueness is determined by the following fields: ${listAsCode(params?.keys)}`,
807
+ 'must contain as many items as values': ({ params, dataPath }) => `The number of ${code(dataPath.split('.').pop())} must match the number of ${code('value')}.
808
+ Currently there are ${params?.current} ${code(dataPath.split('.').pop())} but ${params?.expected} ${code('value')}.`,
809
+ 'is too generic': ({ params }) => `You have the following Product ${code(params?.product?.name)} however,
810
+ you have used a generic ${glossaryTypeLink(params?.term?.termType)} term ${code(params?.term?.name)}.
811
+ Use a more specific term for the ${glossaryTypeLink(params?.term?.termType)} which reflects the specific ${glossaryTypeLink(params?.product?.termType)}.`,
812
+ 'should not use identical terms with different units': ({ params }) => `You have added the same ${code(params?.term?.termType)} in ${params?.units
813
+ ?.map(unit => code(unit))
814
+ .join(' and ')} units.
815
+ If these terms refer to the same ${code(params?.term?.termType)}, this will lead to double counting.${params?.units?.includes('kg N')
816
+ ? ` Instead use the ${termLink({ id: 'nitrogenContent', name: 'Nitrogen Content' })} Property on the ${code('kg')} term to define the nitrogen content of the ${code(params?.term?.termType)}.`
817
+ : ''}`,
818
+ 'is missing required bibliographic information': () => `The automatic bibliography search failed for this Bibliography. Either:
819
+ ${formatList([
820
+ `Manually fill-in the ${r.bold('required')} bibliographic information as per ${schemaLink(SchemaType.Bibliography, 'our schema')}.`,
821
+ `Provide the ${code('documentDOI')} as well as the ${code('title')}.`,
822
+ `Check the ${code('documentDOI')} and ${code('title')} for typos against the ${externalLink('https://www.mendeley.com', 'Mendeley catalogue')}.`
823
+ ], 'decimal')}`,
824
+ 'should be lower than max size': ({ params }) => `The boundary or region is >${params?.expected}km2 and is too large to reliably gap fill Measurements.
825
+ If you are able to use a more specific region or smaller boundary, please do.`,
826
+ 'an excreta input is required when using an excretaManagement practice': ({ dataPath }) => {
827
+ const paths = parseDataPath(dataPath);
828
+ return `Excreta management is the conversion of excreta to any type of product (e.g., another type of excreta or organic fertiliser).
829
+ You have added an ${code('excretaManagement')} Practice to this ${paths[0].label} but there is no excreta Input.
830
+ To represent excreta management, use a ${paths[0].label} with excreta as an Input and ${code('excretaManagement')} as a Practice.`;
831
+ },
832
+ 'only 1 primary product allowed': () => 'There can only be one primary product in each Cycle.',
833
+ 'is not allowed in combination with noTillage': () => `This operation involves tillage, yet you have specified the Practice ${termLink(noTillage)}.
834
+ Either change this operation or the Practice.`,
835
+ 'should contain a tillage practice': () => `We recommend specifying the type of tillage used for this Cycle.
836
+ Please see the ${glossaryTypeLink(TermTermType.tillage)} glossary.`,
837
+ 'must contain a tillage practice': ({ params }) => `If you have specified a tillage Operation, e.g., ${code(params?.term?.name)}, please specify a tillage regime (excluding ${code('noTillage')}).`,
838
+ 'must set value for every tillage practice': () => `Either specify a single ${glossaryTypeLink(TermTermType.tillage)} practice or add a value to all practices.`,
839
+ 'sum not equal to 100% for tillage practices': () => `The sum of ${glossaryTypeLink(TermTermType.tillage)} practices must equal 100%.`,
840
+ 'can only have 1 tillage practice without a value': ({ params }) => `It is not possible for a Cycle to have the following tillage Practices:
841
+ ${(params?.current ?? []).map(({ name }) => name).join(' and ')} at the same time.
842
+ If multiple tillage practices did occur, please specify the percentage of area they occurred on.`,
843
+ 'cannot use no tillage if depth or number of tillages is not 0': () => `This Practice cannot have a value of ${code(100)} if the Tillage depth is higher than ${code(0)} or the Number of tillages is higher than ${code(0)}. Please resolve this conflict.`,
844
+ 'cannot use full tillage if depth or number of tillages is 0': () => `This Practice cannot have a value of ${code(100)} if the Tillage depth is equal to ${code(0)} or the Number of tillages is equal to ${code(0)}. Please resolve this conflict.`,
845
+ 'must use no tillage when number of tillages is 0': () => `You cannot use a tillage term other than No tillage with a value of ${code(100)} if the Number of tillages has been set to ${code(0)}. Please resolve this conflict.`,
846
+ 'can not be linked to the same Cycle': () => 'You can not link an Input to the Impact Assessment of the same Cycle.',
847
+ // deprecated, remove when message is not being used anymore
848
+ 'must be 0 for product value 0': ({ dataPath, params }, errorCount) => `If the amount produced is zero, the ${code(dataPath?.split('.').pop())} of ${errorCount === 1 ? code(params?.term.name) : 'that product'} must also be zero.`,
849
+ 'economicValueShare must be 0 for product value 0': ({ params }, errorCount) => `If the amount produced is zero, the economicValueShare of ${errorCount === 1 ? code(params?.term.name) : 'that product'} must also be zero.`,
850
+ 'revenue must be 0 for product value 0': ({ params }, errorCount) => `If the amount produced is zero, the revenue of ${errorCount === 1 ? code(params?.term.name) : 'that product'} must also be zero.`,
851
+ 'should add a source': ({ params }) => `We recommend adding a Source to all data items.
852
+ This can be done by adding the ${code(params?.current)} field.
853
+ Sources can also be specified for each data item (i.e., each Input, Emission, Product, Practice, Measurement, or Infrastructure).`,
854
+ 'cover crop cycle contains non cover crop product': () => `You have specified that this crop is a cover crop using a Practice.
855
+ A cover crop is defined as "a crop which is left in the field without any fraction harvested by the end of the Cycle".
856
+ The Products of this Cycle are incompatible with this crop being a cover crop.
857
+ Please check the Products or the Practices.`,
858
+ 'should be included in the cycle products': ({ params }, errorCount) => `${errorCount === 1
859
+ ? `The Product ${code(params.product.name)} cannot be found in the Cycle with id ${code(params.node.id)}`
860
+ : 'The Product in the Impact Assessment cannot be found in the linked Cycle'}.
861
+ This could be simply because the product does not exist in the Cycle: in this case, add it to the Cycle or remove the Impact Assessment.
862
+ It could also be because you have multiple Products in the Cycle using the same Term, but having different Properties or different fields:
863
+ in this case, ensure all of the Properties and fields which make the Product unique are copied into the Impact Assessment.`,
864
+ 'multiple ImpactAssessment are associated with the same Product of the same Cycle': () => `Only one Impact Assessment can be related to this Product.
865
+ Please make sure the product is correct or the cycle.id you are referring to is correct.`,
866
+ 'multiple ImpactAssessment are associated with this Product': () => `Only one Impact Assessment can be related to this Product.
867
+ Please make sure the product of the related ImpactAssessments are correct or the ${code('cycle.id')} is correct.`,
868
+ 'no ImpactAssessment are associated with this Product': () => `In cases where Impact Assessment data are being uploaded, the number of Impact Assessments must match the number of Products.
869
+ This ensures the Cycle data matches the Impact Assessment data.
870
+ To fix this, add one Impact Assessment for every Product in the Cycle, following at least the minimum required fields for Impact Assessments.`,
871
+ 'must be less than or equal to land occupation': () => `Land transformation should be less than or equal to land occupation
872
+ (land transformation is the amount of land converted between some date in the past and the current year,
873
+ divided by an amortization period, so it should always be less than land occupation by definition).`,
874
+ 'should be linked to an emission in the Cycle': ({ params }, errorCount) => `${errorCount === 1 ? `${code(params?.term.name)} exists` : 'Some Emissions exist'} in both the Cycle and the Transformation but ${errorCount === 1 ? 'it is' : 'they are'} not linked using ${code('cycle.emissions.X.transformation.term')}. This may be an error.`,
875
+ 'should add an animal production system': ({ params }) => `For animal rearing cycles, we recommend adding a practice to specify the production system.
876
+ This information is used to estimate ruminants' grass intake and enteric methane emissions for all animals.
877
+ You can choose one or more of the following terms: ${listAsCode(params?.expected)}`,
878
+ 'must be set to false when specifying fuel use': ({ params }) => `If fuel for machinery (${listAsCode(params?.allowedValues)}) is used during a Cycle,
879
+ but there is no data on the amount of ${glossaryTypeLink(TermTermType.material)} used for agricultural equipment,
880
+ then ${code('completeness.material')} must be ${code('false')}.
881
+ Here, we mean the quantity of physical material used to construct agricultural machinery,
882
+ which is then depreciated over each Cycle.`,
883
+ 'must be below or equal to 1 for unit in ha': () => `The maximum value for a product with units ${code('ha')} on a Cycle with a ${schemaLink('Cycle#functionalUnit', 'functional unit')} of ${code('1 ha')} is ${code('1')}.
884
+ If you are trying to add a product with the units ${code('kg')}, use a different term.`,
885
+ "pastureGrass key termType must be 'landCover'": ({ params }) => `The Term ${code(params?.term.name)} can't be used as a key for ${termLink(pastureGrass)}.
886
+ Please, use a term with termType ${glossaryTypeLink(TermTermType.landCover)} instead.
887
+ This will help us gap-fill the amount of grass consumed by the herd.`,
888
+ 'the sum of all pastureGrass values must be 100': ({ params }) => `The sum of all ${termLink(pastureGrass)} values must be equal to ${params.expected}%.`,
889
+ 'is outside confidence interval': (error, errorCount, allErrors) => `${unique((allErrors.length ? allErrors : [error]).flatMap(({ dataPath, params }) => params?.outliers?.map(value => `The ${dataPath.includes('product') ? 'product' : 'input'} value ${errorCount === 1 ? `${code(value)} ` : ''}is ${value > params?.min ? 'above' : 'below'} ${params?.threshold ? `${code(`${(params.threshold + (1 - params.threshold) / 2) * 100}%`)} of` : ''} all ${params?.group ? code(params.group) : code(params?.term?.name)} data in ${code(params?.country.name)} on the HESTIA platform.`))).join(r.lineBreak)}${r.lineBreak}
890
+ Please double-check these values before submitting.`,
891
+ 'should be equal to boundary': ({ params }) => `The calculated ${schemaLink('Site#area', 'area')} from ${code('boundary')} is ${code(params?.expected)} ha.
892
+ This may be an error if the boundary represents the area under cultivation.`,
893
+ 'must specify the fate of cropResidue': ({ params }) => `For sites of type ${params.siteType.map(code).join(' or ')},
894
+ if data completeness is marked ${code(true)} for crop residue, the fates and quantities of the residue must be specified.
895
+ ${r.lineBreak}
896
+ The quantity must include both above and below ground crop residue, where the sum of above ground crop residue and below ground crop residue must be greater than zero.
897
+ This can be added using the terms:
898
+ ${formatList([
899
+ `${termLink({ id: 'belowGroundCropResidue', name: 'Below ground crop residue' })}; and`,
900
+ termLink({
901
+ id: 'aboveGroundCropResidueTotal',
902
+ name: 'Above ground crop residue total'
903
+ })
904
+ ])}
905
+ ${r.lineBreak}
906
+ The fate can be added using:
907
+ ${formatList([
908
+ `The ${glossaryTypeLink(TermTermType.cropResidueManagement)} practices; and`,
909
+ `The ${glossaryTypeLink(TermTermType.cropResidue)} products.`
910
+ ])}
911
+ `,
912
+ 'should specify the fate of cropResidue': () => `You have not specified the fate of the crop residue.
913
+ Adding this information will improve emissions and soil carbon stock calculations.
914
+ The fate can be added using:
915
+ ${formatList([
916
+ `The ${glossaryTypeLink(TermTermType.cropResidueManagement)} practices; and/or`,
917
+ `The ${glossaryTypeLink(TermTermType.cropResidue)} products.`
918
+ ])}
919
+ Remember to set ${code('completeness.cropResidue')} to ${code(true)} if both the above and below ground crop residue quantities and fates are specified.`,
920
+ 'should have the same privacy as the related source': ({ params }) => `To link to a ${(params?.defaultSource || params?.source)?.dataPrivate ? 'private' : 'public'} Source,
921
+ this Node needs to be ${(params?.defaultSource || params?.source)?.dataPrivate ? 'private' : 'public'} as well.
922
+ Please set ${code('dataPrivate')}=${code(`${(params?.defaultSource || params?.source)?.dataPrivate}`)}.`,
923
+ 'should not use not relevant model': () => `All emissions should generally have a method tier, which states how the data were modelled or how the data were collected.
924
+ Please see the ${schemaLink('Emission#methodTier', 'schema')} for the options available and definitions of the options.`,
925
+ 'should not use not relevant methodTier': () => `All emissions should generally have a method tier, which states how the data were modelled or how the data were collected.
926
+ Please see the ${schemaLink('Emission#methodTier', 'schema')} for the options available and definitions of the options.`,
927
+ 'animal population must not be complete': () => `This Cycle has ${code('liveAnimals')} or ${code('liveAquaticSpecies')} as Products, ${code('completeness.animalPopulation')} is set to ${code('true')}, but there are no ${code('animals')} blank nodes.
928
+ Either: set ${code('completeness.animalPopulation')} to ${code('false')} or add the animal blank nodes to describe the animal population.`,
929
+ 'ingredients should be complete': ({ params }) => `Data completeness for ${code('ingredient')} must be true for site of type ${params.siteType}`,
930
+ 'must have inputs to represent ingredients': () => `Data completeness for ${code('ingredient')} cannot be true if there are no Inputs.`,
931
+ 'must have inputs representing the forage when set to true': ({ params }) => `This Cycle includes animals ${params?.siteType ? `on ${params.siteType}` : ''} and completeness is set to ${code(true)} for ${code('freshForage')}.
932
+ However, there are no Inputs representing the forage intake by animals in the Cycle (Inputs with ${code('termType')} ${code('freshForage')}" and a ${code('value')}).
933
+ Either, set completeness to ${code(false)} for ${code('freshForage')}, make sure all fresh forage Inputs have a ${code('value')}, or add one or more Inputs representing the forage intake.`,
934
+ 'must specify is it an animal feed': () => `This Cycle is an animal production Cycle and this Input could be an animal feed or could be for another use.
935
+ Please specify the ${schemaLink('Input#isAnimalFeed', 'isAnimalFeed')} field on this Input.
936
+ Knowing the fate is essential to distinguish Inputs between those which are fed to animals and those which are used for other purposes.`,
937
+ 'should specify a treatment when experimentDesign is specified': () => `You have specified that this Source uses an experimental design, but have not specified the treatment for this Cycle.
938
+ Adding the treatment provides important meta-data and will enable greater re-use of these data.
939
+ Add the ${schemaLink('Cycle#treatment', 'treatment')} field to the Cycle.
940
+ Note that the field ${schemaLink('Cycle#commercialPracticeTreatment', 'commercialPracticeTreatment')} becomes required if treatment is specified.`,
941
+ 'should not specify both liveAnimal and animalProduct': () => `You have added both a ${glossaryTypeLink(TermTermType.liveAnimal)} and ${glossaryTypeLink(TermTermType.animalProduct)} term as Products.
942
+ If these terms refer to the same animals, this will lead to double counting:
943
+ instead use a Property on the ${glossaryTypeLink(TermTermType.liveAnimal)} term to define the quantity of liveweight per head, carcass weight per head, etc.`,
944
+ 'should add an excreta product': () => `Animal production Cycles create excreta.
945
+ There should be at least one ${code(TermTermType.excreta)} product in this Cycle.
946
+ Please add an ${code(TermTermType.excreta)} Product to the Cycle.
947
+ You do not need to set a ${code('value')} for the Product, but adding a ${code('value')} is desirable.`,
948
+ 'is not an allowed excreta product': ({ params }) => `This excreta product is not allowed for the animal products added to this Cycle.
949
+ Instead, please add one of the following excreta: ${listAsCode(params.expected)}`,
950
+ 'value should sum to 100 across all values': ({ params }, errorCount) => `${errorCount === 1
951
+ ? `The total value of the following terms should sum up to a value of 100%: ${formatList((params.termIds || []).map(code))}`
952
+ : 'Some of the terms are grouped together, and their total value should sum up to 100%.'}
953
+ Make sure to add any missing term if needed and/or adjust the values of the existing terms you already recorded.`,
954
+ 'value should sum to maximum 100 across all values': ({ params }, errorCount) => `${errorCount === 1
955
+ ? `The total value of the following terms should sum up to a maximum value of 100%: ${formatList((params.termIds || []).map(code))}`
956
+ : 'Some of the terms are grouped together, and their total value should not go over 100%.'}
957
+ Make sure to add any missing term if needed and/or adjust the values of the existing terms you already recorded.`,
958
+ 'rice products not allowed for this water regime practice': ({ params }) => `This practice is not allowed with the products ${params.products?.map(p => code(p.name || p['@id']))}.
959
+ Recording the right water regime for rice ensures methane emissions from flooded fields are correctly recalculated.
960
+ To fix this error, either change the practice or the products.`,
961
+ 'must not be equal to 1 ha': ({ params }) => `${code(CycleFunctionalUnit['1 ha'])} cannot be used as ${schemaLink('Cycle#functionalUnit', 'functionalUnit')} for Cycles linked to Sites with ${schemaLink('Site#siteType', 'siteType')} = ${code(params?.siteType)}.
962
+ Either change the ${schemaLink('Cycle#functionalUnit', 'functionalUnit')} to ${code(CycleFunctionalUnit.relative)} or change the ${schemaLink('Site#siteType', 'siteType')}.`,
963
+ 'should specify a value when HESTIA has a default one': ({ params }) => `As you have not specified a ${code('value')}, we will assume the value is ${code(params?.expected)}.
964
+ If this is incorrect, pelase add a value.`,
965
+ 'should contain at least one management node': ({ params }) => `${params?.termType
966
+ ? `Does not contain information about ${code(params.termType)}.
967
+ Please consider adding a history of ${code(params.termType)} for as many years as possible in the Management blank nodes.`
968
+ : `There is no Management node present in this upload.
969
+ We recommend adding this to the Site, including information about current and historical tillage.
970
+ `}
971
+ We use this information to model CO${r.subscript('2')} emissions from soil organic carbon change.`,
972
+ 'can not be used on this termType': ({ params }) => `This Property can only be used on blank nodes with the following ${code('term.termType')}: ${listAsCode(params.expected)}.`,
973
+ 'not a valid GeoJSON': ({ params }) => `
974
+ ${params?.invalidCoordinates === 'true'
975
+ ? `GeoJSON data should follow the WGS84 datum.
976
+ We have found coordinates outside of the -180 to +180 range which is not possible under WGS84.
977
+ Please check the datum or check for other possible errors.`
978
+ : 'This GeoJSON appears to be invalid.'}
979
+ You can validate your GeoJSON using an online tool like ${r.link('https://geojson.io/', 'geojson.io', false)}.
980
+ If you think this is a mistake, please report the error ${reportIssueLink(Repository.schema, Template.bug)}.`,
981
+ 'should add the term stockingDensityPermanentPastureAverage': ({ params }) => `You did not specify ${externalNodeLink({ '@type': NodeType.Term, '@id': params.expected })}.
982
+ Adding this Practice is essential for Cycles with relative functional unit which happen entirely or partially on permanent pasture.
983
+ Without this information HESTIA cannot calculate animal related emissions.`,
984
+ 'should specify the herd composition': () => `We recommend adding information on the herd composition using the Animal node.
985
+ Without this, we cannot re-model many animal related Inputs, Products, and Emissions.`,
986
+ 'should specify the pregnancy rate': ({ params }) => `For female mammals in their reproductive age, we recommend adding ${externalNodeLink({
987
+ '@type': NodeType.Term,
988
+ '@id': params.expected
989
+ })} as a ${schemaLink('Animal#properties', 'Property')}.
990
+ This is particularly important for ruminants, as the pregnancy rate influences animals' energy requirements and is used in the estimation of grass intake.`,
991
+ 'should specify the a milkYield practice': ({ params }) => `For this Animal, we recommend adding one of the following practices: ${listAsCode(params?.expected)}.
992
+ These practices will be used for estimating grass intake. If not added, default values will be used. `,
993
+ 'should specify a recommended product': ({ params }) => `Based on the Animals present in your Cycles, some Products might be missing: ${listAsCode(params?.expected)}.
994
+ Recording all products is essential to correctly allocate impacts.
995
+ Consider adding the suggested products or updating the product completeness.`,
996
+ 'should set both depthUpper and depthLower': () => `
997
+ We recommend setting both ${schemaLink('Measurement#depthUpper', 'depthUpper')} and ${schemaLink('Measurement#depthLower', 'depthLower')} on this Measurement.`,
998
+ 'must set both depthUpper and depthLower': () => `This measurement can vary substantially with depth.
999
+ We require you set ${schemaLink('Measurement#depthUpper', 'depthUpper')} and ${schemaLink('Measurement#depthLower', 'depthLower')}.
1000
+ Please check the source for depth information and add it.
1001
+ If the source does not have depth information, but you have evidence this measurement reflects the topsoil, set ${code('depthUpper = 0')} and ${code('depthLower = 20')}, and set ${schemaLink('Measurement#methodClassification', 'methodClassification')} to ${code('modelled using other measurements')} and describe this assumption usign ${schemaLink('Measurement#methodClassificationDescription', 'methodClassificationDescription')}.
1002
+ If you have no evidence about the depth of the measurement, delete the data.`,
1003
+ 'multiple cycles on the same site cannot overlap': (error, _, allErrors) => `Cycles taking place on the same Site cannot overlap:
1004
+ ${formatList((allErrors.length ? allErrors : [error]).map(({ params }) => listAsCode(params.ids)))}
1005
+ Please consider any of the following suggestions to resolve this conflict:
1006
+ ${formatList([
1007
+ `You can amend the Cycles' ${schemaLink('Cycle#startDate', 'startDate')} and ${schemaLink('Cycle#endDate', 'endDate')} to ensure that they do not overlap anymore.`,
1008
+ 'You can break down your Cycles into shorter Cycles to ensure that they do not overlap anymore.',
1009
+ `You can merge the overlapping Cycles into one overall Cycle if the Products of each Cycle are grown at the same time on the same Site.
1010
+ This is relevant if intercropping is used, for instance.`,
1011
+ `You can create a different Site for each overlapping Cycle.
1012
+ This is appropriate if a field is divided into different plots, each dedicated to growing a different Product; each plot would be considered a unique Site on the platform.`
1013
+ ])}
1014
+ `,
1015
+ 'cycles linked together cannot be added to the same site': ({ params }) => `Cycles linked together by an Impact Assessment cannot take place on the same Site.
1016
+ Please create different Sites for the following Cycles: ${listAsCode(params.ids)}.`,
1017
+ 'is not an allowed animalProduct': ({ params }) => `The ${glossaryTypeLink(TermTermType.liveAnimal)} producing this Product is not recorded in the ${schemaLink('Cycle#animals', 'Animal')} blank nodes. Either change this product or add the missing ${glossaryTypeLink(TermTermType.liveAnimal)} to the ${schemaLink('Cycle#animals', 'Animal')} blank nodes.
1018
+ The allowed product ids are: ${listAsCode(params?.expected)}`,
1019
+ 'duration must be in specified interval': ({ params: { term } = {} }) => ['shortFallow', 'shortBareFallow'].includes(term['@id'])
1020
+ ? `${code('Short fallow')} is defined as land left fallow for less than 365 days.
1021
+ Please check the ${schemaLink('Management#startDate', 'startDate')} and ${schemaLink('Management#endDate', 'endDate')} on this term and ensure the difference between the two dates is less than 365 days.
1022
+ If it is more than 365 days, consider using the terms ${code('Long fallow')} or ${code('Set aside')} instead.`
1023
+ : ['longFallow', 'longBareFallow'].includes(term['@id'])
1024
+ ? `${code('Long fallow')} is defined as land left fallow for more than a year but less than five years.
1025
+ Please check the ${schemaLink('Management#startDate', 'startDate')} and ${schemaLink('Management#endDate', 'endDate')} on this term and ensure the difference between the two dates falls between 365 and 1826 days.
1026
+ If it is less than 365 days, consider using the term ${code('Short fallow')} instead.
1027
+ If it is more than 1826 days, consider using the term ${code('Set aside')} instead.`
1028
+ : `${code('Set aside')} is defined as the temporary set aside of land for more than five years but less than twenty years.
1029
+ Please check the ${schemaLink('Management#startDate', 'startDate')} and ${schemaLink('Management#endDate', 'endDate')} on this term and ensure the difference between the two dates falls between 1826 and 7305 days.
1030
+ If it is less than 1826 days, consider using the terms ${code('Short fallow')}or ${code('Long fallow')} instead.
1031
+ If it is more than 7305 days, consider using the term ${code('Other natural vegetation')} instead.`,
1032
+ 'must not add the feed input to the Cycle as well': ({ params: { term } = {} }) => `You have added the animal feed Input ${code(term['@id'])} to the Cycle inputs as well, which might lead to double counting.
1033
+ If you do not know the amount of ${code(term['@id'])} given to each Animal, please add the Input to the Cycle only.`,
1034
+ 'must provide value or min and max': ({ params }) => `You have provided only a ${code('max')} without a ${code('min')} or a ${code('value')}${params.term ? ` for ${termLink(params.term)}` : ''}.
1035
+ ${params?.term?.termType === TermTermType.measurement
1036
+ ? `This Measurement has additional restrictions and needs either a ${code('value')} or both a ${code('min')} and ${code('max')} specified.
1037
+ Please consider adding both the ${code('min')} and the ${code('max')}, or delete the Measurement.`
1038
+ : ''}`,
1039
+ 'must specify water type for ponds': ({ params }) => `For Sites with ${schemaLink('Site#siteType', 'siteType')} = ${code('pond')}, you must specify the ${externalNodeLink({
1040
+ '@type': NodeType.Term,
1041
+ '@id': 'waterSalinity',
1042
+ name: 'Water salinity'
1043
+ })} or the water type (${externalNodeLink({
1044
+ '@type': NodeType.Term,
1045
+ '@id': 'salineWater',
1046
+ name: 'Saline water'
1047
+ })} / ${externalNodeLink({ '@type': NodeType.Term, '@id': 'freshWater', name: 'Fresh water' })} / ${externalNodeLink({
1048
+ '@type': NodeType.Term,
1049
+ '@id': 'brackishWater',
1050
+ name: 'Brackish water'
1051
+ })}).
1052
+ This will allow us to calculate methane emissions from constructed waterbodies.
1053
+ ${params?.products
1054
+ ? `Note: our gap-filling model can only work when the lookup ${code('defaultSalinity')} is set for all products,
1055
+ but we are currently missing a value for: ${joinValues(params.products)}.`
1056
+ : ''}`,
1057
+ 'invalid water salinity': ({ params }) => `The water type ${params?.current} is not consistent with the Water salinity you specified.
1058
+ Make sure the Water salinity value is correct and either fix it or update the water type accordingly.`,
1059
+ 'measurements for the same termType must be consistent': ({ params }) => `The information you have provided for ${code(params.termType)} Measurements is not consistent across all Measurements.
1060
+ Please ensure that all your ${code(params.termType)} Measurements have a ${code('dates')} field if at least one ${code(params.termType)} Measurement has a ${code('dates')} field specified.
1061
+ Please also ensure that all your ${code(params.termType)} Measurements have ${code('depthUpper')} and ${code('depthLower')} fields if at least one ${code(params.termType)} Measurement has ${code('depthUpper')} and ${code('depthLower')} fields specified.`,
1062
+ 'must add substrate inputs': () => 'The substrate must be specified when a substrate-based protected cropping system has been added.',
1063
+ 'should be equal to cycleDuration for crop': ({ params }) => `For temporary crop production Cycles, ${schemaLink('Cycle#siteDuration', 'siteDuration')} must represent the period from harvest of the previous crop to harvest of the current crop.
1064
+ Here, you have stated that ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} represents the period from
1065
+ ${code(params?.current)} to harvest of the current crop, and set ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} as not equal to ${schemaLink('Cycle#siteDuration', 'siteDuration')}.
1066
+ Please check the affected fields for errors.`,
1067
+ 'should be greater than cycleDuration for crop': ({ params }) => `For temporary crop production Cycles, ${schemaLink('Cycle#siteDuration', 'siteDuration')} must represent the period from harvest of the previous crop to harvest of the current crop.
1068
+ Here, you have stated that ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} represents the period from
1069
+ ${code(params?.current)} to harvest of the current crop, and set ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} as greater than ${schemaLink('Cycle#siteDuration', 'siteDuration')}.
1070
+ Please check the affected fields for errors.`,
1071
+ 'should be less than cycleDuration for animals': () => `You have specified that ${schemaLink('Cycle#siteDuration', 'siteDuration')} is greater than or equal to ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} for your Cycle.
1072
+ However, because you indicated that there were ${schemaLink('Cycle#otherSites', 'otherSites')} used by the animals, this is not possible.
1073
+ When ${schemaLink('Cycle#otherSites', 'otherSites')} is set, the Cycle's ${schemaLink('Cycle#siteDuration', 'siteDuration')} must be lower than the ${schemaLink('Cycle#cycleDuration', 'cycleDuration')}.
1074
+ This is because the animals are also spending time on the other Sites.
1075
+ Please fix the ${schemaLink('Cycle#siteDuration', 'siteDuration')} or the ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} accordingly.`,
1076
+ 'should be equal to cycleDuration - otherSitesDuration for animals': () => `The values you have provided for your Cycle's ${schemaLink('Cycle#siteDuration', 'siteDuration')} and ${schemaLink('Cycle#otherSitesDuration', 'otherSitesDuration')} do not match the ${schemaLink('Cycle#cycleDuration', 'cycleDuration')}.
1077
+ The sum of ${schemaLink('Cycle#siteDuration', 'siteDuration')} and all ${schemaLink('Cycle#otherSitesDuration', 'otherSitesDuration')} must equal ${schemaLink('Cycle#cycleDuration', 'cycleDuration')}.
1078
+ Please amend the values of ${schemaLink('Cycle#siteDuration', 'siteDuration')}, ${schemaLink('Cycle#otherSitesDuration', 'otherSitesDuration')} or ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} accordingly.`,
1079
+ 'should be equal to cycleDuration for animals': () => `You have specified that ${schemaLink('Cycle#siteDuration', 'siteDuration')} is not equal to ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} for your Cycle.
1080
+ However, ${schemaLink('Cycle#siteDuration', 'siteDuration')} must equal ${schemaLink('Cycle#cycleDuration', 'cycleDuration')} for animal Cycles with a single associated Site.
1081
+ Please ensure that the ${schemaLink('Cycle#siteDuration', 'siteDuration')} is equal to the ${schemaLink('Cycle#cycleDuration', 'cycleDuration')}.`,
1082
+ 'is required when using other model': () => `If you are using the term ${code('Other model')}, please provide a short description of the model in ${code('methodModelDescription')}, including any references to model documentation.`,
1083
+ 'should add the term productivePhasePermanentCrops': () => `You need to specify whether the permanent crop is in its productive phase when the value of the primary product is ${code('0')}.
1084
+ To do this, add the ${termLink({
1085
+ id: 'productivePhasePermanentCrops',
1086
+ name: 'Productive phase (permanent crops)'
1087
+ })} practice and set a value of ${code('true')} or ${code('false')}.`,
1088
+ 'must have a single inputs of termType waste': () => `Can only be linked to a single ${glossaryTypeLink(TermTermType.waste)} input.`,
1089
+ 'must be below maximum cycleDuration': ({ params }) => `The maximum duration for this crop production cycle is ${params.limit} days,
1090
+ but the calculated duration from the end date and start date provided is ${params.current}.`,
1091
+ 'should add the term pastureGrass': () => `For Cycles with ${code('siteType=permanent pasture')} we recommend specifying the type(s) of ${code('Pasture grass')} grown.
1092
+ This is particularly important for ruminants and will allow for a more accurate estimate of the amount of grass grazed.`,
1093
+ 'should add the fields for a relative cycle': ({ params }) => `We strongly recommend setting ${params?.expected
1094
+ ?.map((param) => code(param))
1095
+ .join('and')} on all other sites for any uploads with a ${code('relative')} functional unit. This will allow calculation of land occupation and land transformation.`,
1096
+ 'must not be blank if complete': ({ params }) => `If completeness is set to true for ${schemaLink(`Completeness#${params.expected}`, params.expected)}, none of the data in the completeness area can have missing or blank values.`,
1097
+ 'primary product cannot be an input': ({ params }) => params.siteType === SiteSiteType['agri-food processor']
1098
+ ? `For a processing cycle, the primary Product must not also be an Input into the cycle.
1099
+ This ensures we reflect the full process rather than just single sub-processes.
1100
+ Use Transformations if needed to represent additional processing steps rather than new Cycles.`
1101
+ : `The primary Product must not also be an Input into the cycle.`,
1102
+ 'must have a primary processing operation': () => `For processing cycles, a primary Operation must be specified, it should reflect the main process in the Cycle.`,
1103
+ 'primaryPercent not allowed on this siteType': ({ params }) => `The ${schemaLink('Practice#primaryPercent', 'primaryPercent')} can only be used on ${schemaLink('Site#siteType', 'siteType')} ${params?.expected?.map(code).join(' or ')}.`,
1104
+ 'primaryPercent not allowed on this practice': () => `This practice is not a processing operation.
1105
+ You can only add the ${schemaLink('Practice#primaryPercent', 'primaryPercent')} field to terms with ${code('termType')} = ${code(TermTermType.operation)} and lookup ${code('isProcessingOperation')} = ${code(true)}.`,
1106
+ 'at least one landCover practice must match an equivalent product': ({ params }) => `To ensure there is no conflicting data between your Cycle and Products, your specified ${code(TermTermType.landCover)} Practices must match a Product.
1107
+ ${formatList([
1108
+ `Ensure at least one Practice matches a Product (e.g., ${listAsCode(params?.current)} matching ${listAsCode(params?.expected)}). Check ${code('landCoverTermId')} in the glossary for equivalents.`,
1109
+ `If specifying fallow periods, you must also include the Product's equivalent Practice with a ${code('startDate')} and ${code('endDate')}.`,
1110
+ `Alternatively, do not set these ${code(TermTermType.landCover)} practices, and they will be gap filled automatically from products.`
1111
+ ])}`,
1112
+ 'should match the site management node value': () => `The data you have uploaded in the Management node are not consistent with the data provided in the Cycles.
1113
+ If the same ${code('term')} is used between the same ${code('startDate')} and ${code('endDate')}, they must have the same ${code('value')} in ${code('Management')} and in the Cycles.
1114
+ Please check your upload and ensure that the data is consistent throughout.`,
1115
+ 'saplings cannot be used as an input here': () => `To input ${code('Saplings')} into a plantation crop Cycle, you must use ${externalNodeLink({
1116
+ '@type': NodeType.Term,
1117
+ '@id': 'saplingsDepreciatedAmountPerCycle',
1118
+ name: 'Saplings, depreciated amount per Cycle'
1119
+ })}.
1120
+ This input should be present in each plantation Cycle in your upload, with the ${code('value')} being the total number of ${code('Saplings')} used divided by the number of possible Cycles in the plantation's lifespan.
1121
+ See the ${guideLink('guide-file-upload-special-cases', 'special upload case')} for more details.`,
1122
+ 'must be linked to a verified country-level Impact Assessment': ({ params }) => [
1123
+ 'This Input must be linked to a country-level verified aggregation, but none could be found.',
1124
+ params?.current ? 'Note: using a verified World aggregation is not valid.' : ''
1125
+ ]
1126
+ .filter(Boolean)
1127
+ .join('\n'),
1128
+ 'must contain water inputs': () => `If ${schemaLink('Completeness#water', 'completeness.water')}=${code(true)} for irrigated Cycles, you must provide ${glossaryTypeLink(TermTermType.water)} inputs.
1129
+ Otherwise, set ${schemaLink('Completeness#water', 'completeness.water')}=${code(false)}.`,
1130
+ 'must be true for rainfed cycles': () => 'For rainfed cycles, water completeness should be TRUE as other water inputs are negligible to irrigation quantities.',
1131
+ 'must not set rainfed with water inputs': ({ params }) => `For Cycles with rainfed practices (${listAsCode(params.termIds)}), the sum of the ${code(TermTermType.water)} inputs must be below ${code(params.expected)}m3.
1132
+ If the ${code(TermTermType.water)} inputs are correct, then the ${code(TermTermType.waterRegime)} practices should be set as one of the "irrigated" terms.`,
1133
+ 'management date must be before cycle start date': () => `The Site Management's dates must not overlap with any of the Cycles' dates.
1134
+ The Site Management must only be used to record historical information about years preceding your first Cycle.
1135
+ If you are trying to specify land management or practices that take place during the period covered by your Cycles, please do so directly in the Cycles, using the Product or Practice nodes.
1136
+ Please refer to our Guide section on special case uploads for more information: ${guideLink('guide-file-upload-special-cases', 'special upload case')}.
1137
+ Note: if the first Cycle ${code('startDate')} is not provided, the ${code('cycleDuration')} can be used to set the earliest date of overlap.
1138
+ If the first Cycle ${code('cycleDuration')} is not provided, the lookup of the ${code('maximumCycleDuration')} primary Product can be used to set the earliest date of overlap.`,
1139
+ 'must be in the same format as endDate': () => `The date format does not match the format of ${code('endDate')}. You must use the same date format for both fields.`,
1140
+ 'must be at least equal to the minimum value': ({ params: { min } }) => `Must be at least equal to ${min}`,
1141
+ 'must equal to endDate - startDate in days': ({ params: { expected } }) => `Must be equal to ${code('endDate')} - ${code('startDate')} in days (inclusive of the ${code('startDate')} and ${code('endDate')} days) (~${expected})`,
1142
+ 'must equal to endDate - startDate in decimal years': ({ params: { expected } }) => `Must be equal to ${code('endDate')} - ${code('startDate')} in years (~${expected})`,
1143
+ 'must be more than or equal to other crop residues': ({ params: { expected } }) => `Must be more than or equal to sum(${expected.join(' + ')})`,
1144
+ 'should add missing inputs': ({ params: { expected } }) => `Should add missing inputs: ${listAsCode(expected)}`,
1145
+ 'the property value type is incorrect': ({ params: { expected } }) => `The value must be a ${expected}.`,
1146
+ 'the node value type is incorrect': ({ params: { expected } }) => `The value must be a ${expected}.`,
1147
+ 'can not be linked to the same type': ({ params: { current } }) => `Cannot be linked to the same ${current}.`,
1148
+ 'must be lower or equal to siteArea': () => `The value of ${code('harvestedArea')} cannot be greater than ${code('siteArea')}.
1149
+ This is because ${code('harvestedArea')} corresponds to the extent of the overall ${code('siteArea')} that is actually harvested.
1150
+ Please amend the value of ${code('harvestedArea')} accordingly.`,
1151
+ 'must be above the minimum': ({ params: { min } }) => `Must be above ${min}.`,
1152
+ 'must be below the maximum': ({ params: { max } }) => `Must be below ${max}.`,
1153
+ 'must be linked to waste input': () => `The term ${code('ionisingCompoundsToAirInputsProduction')} can only have 1 entry of ${code('termType')}: ${glossaryTypeLink(TermTermType.waste)}, so that the ${code('value')} field can represent the amount of units of that waste term.`,
1154
+ 'must not have background methodTier': ({ params: { allowedValues } }) => `Only background emissions (e.g., "Inputs Production") can have ${code('methodTier')}=${EmissionMethodTier.background}.
1155
+ You must instead choose one of the following values: ${listAsCode(allowedValues)}`,
1156
+ 'should be within acceptable range of the lookup value': ({ params }) => `The ${code(params.term['@id'])} value is ${code(params.delta)}% higher than our default value for this Term (${code(params.expected)}).
1157
+ Please check your data as this may be an error. ${params?.threshold ? `Our threshold is ±${threshold(params?.threshold)}.` : ''}`,
1158
+ 'should add missing properties': ({ params: { expected, termType } }) => [
1159
+ `We recommend adding the following properties: ${listAsCode(expected)}.`,
1160
+ termType === TermTermType.liveAnimal
1161
+ ? `These properties influence energy requirements and are used for estimating grass intake.`
1162
+ : null,
1163
+ termType === TermTermType.animalManagement ? `These properties will be used for estimating grass intake.` : null,
1164
+ 'If not added, default values might be used.'
1165
+ ]
1166
+ .filter(Boolean)
1167
+ .join('\n'),
1168
+ 'should set the depth': ({ params }) => `The value of Emission ${code(params.term['@id'])} can change significantly based on the soil depth taken into account.
1169
+ Please consider adding ${schemaLink('Emission#depthUpper', 'depthUpper')} and ${schemaLink('Emission#depthLower', 'depthLower')} fields to the Emission.`,
1170
+ 'must include practices for plantation weighting': () => `For permanent crop aggregations, we must be able to weight based on the plantation phase.
1171
+ This means practices ${externalNodeLink({
1172
+ '@type': NodeType.Term,
1173
+ '@id': 'plantationLifespan'
1174
+ })} and ${externalNodeLink({ '@type': NodeType.Term, '@id': 'plantationProductiveLifespan' })} must have values.`
1175
+ };
1176
+ const requiredPropertyError = (message, error) => {
1177
+ const field = message.split("'")[1].replace("'", '');
1178
+ const nodeType = dataPathLabel(error?.dataPath);
1179
+ return `You are missing the following required field: ${code(field)}. ${nodeType && nodeType.toLowerCase() !== 'completeness'
1180
+ ? `Either: add ${code(field)}, or if you if you did not intend to add this ${nodeType}, please remove all fields on this ${nodeType}.`
1181
+ : ''}`.trim();
1182
+ };
1183
+ const parseDefaultMessage = (message, error) => r.capitalizeFirst(message?.startsWith('should have required property') ? requiredPropertyError(message, error) : message);
1184
+ const formatCustomErrorMessage = (message, error, allErrors = []) => {
1185
+ const formattedMessage = customErrorMessage?.[message]
1186
+ ? customErrorMessage[message](error, allErrors.length || 1, allErrors)
1187
+ : parseDefaultMessage(message, error);
1188
+ return formattedMessage ? r.wrapMessage(formattedMessage) : '';
1189
+ };
1190
+ const formatError = (error, allErrors = []) => error
1191
+ ? {
1192
+ level: 'error',
1193
+ ...error,
1194
+ message: formatCustomErrorMessage(error.message, error, allErrors)
1195
+ }
1196
+ : undefined;
1197
+ const locationLabel = (dataPath = '') => {
1198
+ const labels = parseDataPath(dataPath).map(({ label }) => label);
1199
+ return labels.length ? labels.join(' › ') : undefined;
1200
+ };
1201
+ /**
1202
+ * Formats a list of validation errors into structured, framework-agnostic objects.
1203
+ * Each error is formatted on its own (single-error wording).
1204
+ */
1205
+ const formatErrors = (errors = []) => errors
1206
+ .map((error) => {
1207
+ const formatted = formatError(error, [error]);
1208
+ return formatted && formatted.message
1209
+ ? {
1210
+ level: (formatted.level || 'error'),
1211
+ message: formatted.message,
1212
+ location: locationLabel(error.dataPath),
1213
+ dataPath: error.dataPath,
1214
+ keyword: error.keyword
1215
+ }
1216
+ : null;
1217
+ })
1218
+ .filter((formatted) => !!formatted);
1219
+ return {
1220
+ parseDataPath,
1221
+ dataPathLabel,
1222
+ migrationErrorMessage,
1223
+ formatCustomErrorMessage,
1224
+ formatError,
1225
+ formatErrors,
1226
+ locationLabel
1227
+ };
1228
+ };
1229
+ const errorHasError = (error) => error && (error.level === 'error' || !error.level);
1230
+ const errorHasWarning = (error) => error && error.level === 'warning';
1231
+ const isMissingPropertyError = ({ params }) => !!params && 'missingProperty' in params;
1232
+ const isMissingOneOfError = ({ keyword, schemaPath }) => keyword === 'required' && (schemaPath || '').includes('oneOf');
1233
+ const isFailingKeywordError = ({ params }) => !!params && 'failingKeyword' in params;
1234
+ const filterError = (error) => [isFailingKeywordError].every(func => !func(error));
1235
+ const missingNodeErrors = (errors) => errors.filter(({ keyword, params }) => keyword === 'required' && (params?.missingProperty === '@type' || params?.missingProperty === '@id'));
1236
+
1237
+ const htmlListLine = (value) => `<li class="is-pre-wrap">${value}</li>`;
1238
+ const htmlRenderer = {
1239
+ code: text => `<code>${text}</code>`,
1240
+ link: (href, text, newTab = true) => `<a href="${href}"${newTab ? ' target="_blank"' : ''}>${text}</a>`,
1241
+ list: (values, style = 'disc') => `<ul class="is-pl-3 is-list-style-${style} is-nowrap">${values.flat().map(htmlListLine).join('')}</ul>`,
1242
+ paragraph: text => `<p>${text}</p>`,
1243
+ bold: text => `<b>${text}</b>`,
1244
+ subscript: text => `<sub>${text}</sub>`,
1245
+ underline: text => `<u>${text}</u>`,
1246
+ lineBreak: '<br/>',
1247
+ inlineSeparator: '<span>, </span>',
1248
+ capitalizeFirst: text => `<span class="is-capitalized-first-letter">${text}</span>`,
1249
+ wrapMessage: text => `<p class="is-normal">${text}</p>`
1250
+ };
1251
+ const capitalize = (text) => (text ? text.charAt(0).toUpperCase() + text.slice(1) : text);
1252
+ /**
1253
+ * Collapses the incidental whitespace introduced by the catalog's multi-line template
1254
+ * literals while preserving the line breaks that carry meaning (lists, explicit breaks).
1255
+ */
1256
+ const normalizeText = (text) => text
1257
+ .replace(/[ \t]+/g, ' ')
1258
+ .replace(/ *\n */g, '\n')
1259
+ .replace(/\n{3,}/g, '\n\n')
1260
+ .trim();
1261
+ const textRenderer = {
1262
+ code: text => `\`${text}\``,
1263
+ link: (href, text) => `[${text}](${href})`,
1264
+ list: (values, style = 'disc') => `\n${values
1265
+ .flat()
1266
+ .map((value, index) => `${style === 'decimal' ? `${index + 1}.` : '-'} ${value}`)
1267
+ .join('\n')}\n`,
1268
+ paragraph: text => `${text}\n`,
1269
+ bold: text => `**${text}**`,
1270
+ subscript: text => `${text}`,
1271
+ underline: text => `${text}`,
1272
+ lineBreak: '\n',
1273
+ inlineSeparator: ', ',
1274
+ capitalizeFirst: capitalize,
1275
+ wrapMessage: normalizeText
1276
+ };
1277
+
1278
+ const DEFAULT_BASE_URL = 'https://www.hestia.earth';
1279
+ /**
1280
+ * Static, browser-free default configuration pointing at the public HESTIA platform.
1281
+ */
1282
+ const defaultUrlConfig = {
1283
+ baseUrl: () => DEFAULT_BASE_URL,
1284
+ glossaryBaseUrl: () => `${DEFAULT_BASE_URL}/glossary`,
1285
+ schemaBaseUrl: () => `${DEFAULT_BASE_URL}/schema/${SCHEMA_VERSION}`
1286
+ };
1287
+ /**
1288
+ * Builds a configuration from a single base URL.
1289
+ */
1290
+ const urlConfig = (baseUrl = DEFAULT_BASE_URL) => ({
1291
+ baseUrl: () => baseUrl,
1292
+ glossaryBaseUrl: () => `${baseUrl}/glossary`,
1293
+ schemaBaseUrl: () => `${baseUrl}/schema/${SCHEMA_VERSION}`
1294
+ });
1295
+
1296
+ /**
1297
+ * Framework-agnostic error formatting.
1298
+ *
1299
+ * This entry point has no Angular (or Bulma) dependency and can be imported from any
1300
+ * TypeScript package to turn raw HESTIA validation errors into formatted messages.
1301
+ *
1302
+ * Quick start (structured plain text):
1303
+ *
1304
+ * import { formatErrors } from '@hestia-earth/ui-components/file-errors';
1305
+ * const formatted = formatErrors(validationErrors);
1306
+ * // => [{ level, message, location, dataPath, keyword }, ...]
1307
+ *
1308
+ * Custom presentation is achieved by passing a renderer (e.g. `htmlRenderer`) and/or a
1309
+ * url configuration, or by building a formatter directly with `createErrorFormatter`.
1310
+ */
1311
+ /**
1312
+ * Formats a list of validation errors into structured, framework-agnostic objects.
1313
+ */
1314
+ const formatErrors = (errors = [], options = {}) => createErrorFormatter({
1315
+ renderer: options.renderer ?? textRenderer,
1316
+ urls: options.urls ?? defaultUrlConfig
1317
+ }).formatErrors(errors);
1318
+
1319
+ /**
1320
+ * Generated bundle index. Do not edit.
1321
+ */
1322
+
1323
+ export { Repository, Template, createErrorFormatter, defaultUrlConfig, errorHasError, errorHasWarning, filterError, formatErrors, guidePageId, htmlRenderer, isMigrationError, isMissingOneOfError, isMissingPropertyError, missingNodeErrors, reportIssueUrl, textRenderer, urlConfig };
1324
+ //# sourceMappingURL=hestia-earth-ui-components-file-errors.mjs.map