PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.12
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.12
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Modules / Ai / AiFormBuilder.php

AiFormBuilder.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.12, at app/Modules/Ai/AiFormBuilder.php

777 lines 26.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Modules\Ai;
4
5 defined('ABSPATH') || die;
6
7 use Exception;
8 use FluentForm\App\Helpers\Helper;
9 use FluentForm\App\Models\Form;
10 use FluentForm\App\Models\FormMeta;
11 use FluentForm\App\Modules\Acl\Acl;
12 use FluentForm\App\Modules\Form\FormFieldsParser;
13 use FluentForm\App\Modules\Payments\PaymentHelper;
14 use FluentForm\App\Services\FluentConversational\Classes\Converter\Converter;
15 use FluentForm\App\Services\Form\FormService;
16 use FluentForm\Framework\Helpers\ArrayHelper as Arr;
17 use FluentForm\Framework\Support\Sanitizer;
18
19 class AiFormBuilder extends FormService
20 {
21 private $allDefaultFields = [];
22
23 public function __construct()
24 {
25 parent::__construct();
26 add_action('wp_ajax_fluentform_ai_create_form', [$this, 'buildForm'], 11, 0);
27 }
28
29 public function buildForm()
30 {
31 try {
32 Acl::verify('fluentform_forms_manager');
33 $form = $this->generateForm($this->app->request->all());
34 $form = $this->prepareAndSaveForm($form);
35 wp_send_json_success([
36 'formId' => $form->id,
37 'redirect_url' => admin_url(
38 'admin.php?page=fluent_forms&form_id=' . $form->id . '&route=editor'
39 ),
40 'message' => __('Successfully created a form.', 'fluentform'),
41 ], 200);
42 } catch (Exception $e) {
43 wp_send_json_error([
44 'message' => $e->getMessage(),
45 ], 422);
46 }
47 }
48
49 /**
50 * Map the AI-generated field list into a persisted form.
51 *
52 * @param array $form
53 * @return Form|\FluentForm\Framework\Database\Query\Builder
54 * @throws Exception
55 */
56 protected function prepareAndSaveForm($form)
57 {
58 $allFields = $this->getDefaultFields();
59 $fluentFormFields = [];
60 $fields = Arr::get($form, 'fields', []);
61 $isConversational = Arr::isTrue($form, 'is_conversational');
62 $customCss = Arr::get($form, 'custom_css', '');
63 $hasStep = false;
64 $lastFieldIndex = count($fields) - 1;
65
66 $disableFields = array_keys($this->getDisabledComponents());
67 foreach ($fields as $index => $field) {
68 if (count($field) == 1) {
69 $field = reset($field);
70 }
71 if ($element = $this->resolveInput($field)) {
72 if (in_array($element, $disableFields)) {
73 continue;
74 }
75 if (!$hasStep && 'form_step' === $element) {
76 if (0 === $index || $lastFieldIndex === $index) {
77 continue;
78 }
79 $hasStep = true;
80 }
81 $fluentFormFields[] = $this->processField($element, $field, $allFields);
82 }
83 }
84 $fluentFormFields = array_filter($fluentFormFields);
85 if (!$fluentFormFields) {
86 throw new Exception(esc_html__('Empty form. Please try again!', 'fluentform'));
87 }
88 $title = Arr::get($form, 'title', '');
89 return $this->saveForm($fluentFormFields, $title, $hasStep, $isConversational, $customCss);
90 }
91
92 /**
93 * Send the prompt to the AI service and return the decoded form fields.
94 *
95 * @param array $args
96 * @return array response form fields
97 * @throws Exception
98 */
99 protected function generateForm($args)
100 {
101 $aiModel = Arr::get($args, 'ai_model', 'default');
102 $isUsingChatGpt = Helper::hasPro() && 'chat_gpt' == $aiModel && class_exists('FluentFormPro\classes\Chat\ChatFormBuilder');
103
104 if ($isUsingChatGpt) {
105 (new \FluentFormPro\classes\Chat\ChatFormBuilder())->buildForm();
106 }
107
108 $paymentSetting = PaymentHelper::getPaymentSettings();
109 $queryArgs = [
110 'user_prompt' => $this->getUserPrompt($args),
111 'site_url' => site_url(),
112 'site_title' => get_bloginfo('name'),
113 'site_locale' => determine_locale(),
114 'has_pro' => Helper::hasPro(),
115 'has_payment' => 'yes' == $paymentSetting['status'],
116 'request_id' => uniqid('ff_ai_'),
117 'save_usage' => apply_filters('fluentform/ai_save_usage', true),
118 ];
119
120 $result = (new FluentFormAIAPI())->makeRequest($queryArgs);
121
122 if (is_wp_error($result)) {
123 throw new Exception(esc_html($result->get_error_message()));
124 }
125
126 $response = trim(Arr::get($result, 'response', ''), '"');
127 // preg_match() returns 0 when there is no fence and false only on error,
128 // so this must test for an actual match — otherwise an unfenced (and
129 // perfectly valid) JSON reply is replaced with an empty string.
130 if (1 === preg_match('/```json(.*?)```/s', $response, $matches)) {
131 $response = trim($matches[1]);
132 }
133
134 $decoded = json_decode($response, true);
135 if (json_last_error() !== JSON_ERROR_NONE || empty($decoded) || empty($decoded['fields'])) {
136 throw new Exception(esc_html__('Invalid response: Please try again!', 'fluentform'));
137 }
138 return $this->applyPromptHints($decoded, $args);
139 }
140
141 protected function getDefaultFields()
142 {
143 if ($this->allDefaultFields) {
144 return $this->allDefaultFields;
145 }
146 $components = $this->app->make('components');
147 $this->app->doAction('fluentform/editor_init', $components);
148 $editorComponents = $components->toArray();
149 // Re-key by element name. The palette groups are keyed by element in
150 // DefaultElements.php, but Components::sort() renumbers them 0..n for
151 // the editor's JSON contract - so whether these arrive keyed or as a
152 // list depends on whether anything rendered the palette earlier in the
153 // request. resolveInput() matches on the key, so normalise here.
154 $general = array_column(Arr::get($editorComponents, 'general', []), null, 'element');
155 $advanced = array_column(Arr::get($editorComponents, 'advanced', []), null, 'element');
156 $container = Arr::get($editorComponents, 'container', []);
157
158 // Apply filter to get additional components
159 // The second parameter (true) is passed to prevent field loss when form ID is falsy loss some field
160 $editorComponents = apply_filters('fluentform/editor_components', [], true);
161 if ($generalExtra = Arr::get($editorComponents, 'general')) {
162 $generalExtra = array_column($generalExtra, null, 'element');
163 $general = array_merge($general, $generalExtra);
164 }
165
166 if ($advancedExtra = Arr::get($editorComponents, 'advanced')) {
167 $advancedExtra = array_column($advancedExtra, null, 'element');
168 $advanced = array_merge($advanced, $advancedExtra);
169 }
170
171 $payments = Arr::get($editorComponents, 'payments', []);
172 $payments = array_column($payments, null, 'element');
173 $this->allDefaultFields = array_merge($general, $payments, $advanced, ['container' => $container]);
174 return $this->allDefaultFields;
175 }
176
177 protected function processField($element, $field, $allFields)
178 {
179 if ('container' == $element) {
180 return $this->resolveContainerFields($field, $allFields);
181 }
182
183 $matchedField = Arr::get($allFields, $element);
184 if (!$matchedField) {
185 return [];
186 }
187 $formatField = $matchedField;
188 if ($settings = Arr::get($field, 'settings')) {
189 // Replace 'label' with 'admin_field_label' if 'label' is shorter
190 if (isset($settings['label']) && $adminFieldLabel = Arr::get($settings, 'admin_field_label')) {
191 if (strlen($settings['label']) < strlen($adminFieldLabel)) {
192 $settings['label'] = $adminFieldLabel;
193 }
194 }
195 $formatField['settings'] = wp_parse_args($settings, $matchedField['settings']);
196 }
197 if ($attributes = Arr::get($field, 'attributes')) {
198 $formatField['attributes'] = wp_parse_args($attributes, $matchedField['attributes']);
199 }
200
201 $formatField['uniqElKey'] = 'el_' . uniqid();
202
203 if ('form_step' === $element) {
204 return $formatField;
205 }
206
207 if ($fieldName = Arr::get($field, 'attributes.name')) {
208 $formatField['attributes']['name'] = $fieldName;
209 }
210
211 if ($options = $this->getOptions(Arr::get($field, 'options'))) {
212 if (isset($formatField['settings']['advanced_options'])) {
213 $formatField['settings']['advanced_options'] = $options;
214 }
215 if ('ratings' == $element) {
216 $formatField['options'] = array_column($options, 'label', 'value');
217 }
218 }
219
220 if ('rangeslider' == $element) {
221 if ($min = Arr::get($field, 'min')) {
222 $formatField['attributes']['min'] = intval($min);
223 }
224 if ($max = intval(Arr::get($field, 'max', 10))) {
225 $formatField['attributes']['max'] = $max;
226 }
227 }
228
229 if (in_array($element, ['input_name', 'address']) && $fields = Arr::get($field, 'fields')) {
230 foreach ($formatField['fields'] as $name => &$field) {
231 if ($targetAttributes = Arr::get($fields, "$name.attributes")) {
232 $field['attributes'] = wp_parse_args($targetAttributes, $field['attributes']);
233 }
234 if ($targetSettings = Arr::get($fields, "$name.settings")) {
235 $field['settings'] = wp_parse_args($targetSettings, $field['settings']);
236 }
237 }
238 }
239
240 return $formatField;
241 }
242
243 protected function resolveInput($field)
244 {
245 if (!is_array($field)) {
246 return false;
247 }
248 $element = Arr::get($field, 'element');
249 $allElements = array_keys($this->getDefaultFields());
250 if (in_array($element, $allElements)) {
251 return $element;
252 }
253
254 $type = Arr::get($field, 'type');
255 if (!$type) {
256 return false;
257 }
258
259 $searchTags = fluentformLoadFile('Services/FormBuilder/ElementSearchTags.php');
260 $form = ['type' => ''];
261 $form = json_decode(json_encode($form));
262 $searchTags = apply_filters('fluentform/editor_element_search_tags', $searchTags, $form);
263 foreach ($searchTags as $inputKey => $tags) {
264 if (array_search($type, $tags) !== false) {
265 return $inputKey;
266 } else {
267 foreach ($tags as $tag) {
268 if (strpos($tag, $type) !== false) {
269 return $inputKey;
270 }
271 }
272 }
273 }
274 return false;
275 }
276
277 protected function getOptions($options = [])
278 {
279 $formattedOptions = [];
280 if (empty($options) || !is_array($options)) {
281 return $options;
282 }
283 foreach ($options as $key => $option) {
284 if (is_string($option) || is_numeric($option)) {
285 $value = $label = $option;
286 } elseif (is_array($option)) {
287 $label = Arr::get($option, 'label');
288 $value = Arr::get($option, 'value');
289 } else {
290 continue;
291 }
292 if (!$value || !$label) {
293 $value = $value ?? $label;
294 $label = $label ?? $value;
295 }
296 if (!$value || !$label) {
297 continue;
298 }
299 $formattedOptions[] = [
300 'label' => $label,
301 'value' => $value,
302 ];
303 }
304
305 return $formattedOptions;
306 }
307
308 protected function getBlankFormConfig()
309 {
310 $attributes = ['type' => 'form', 'predefined' => 'blank_form'];
311 $customForm = Form::resolvePredefinedForm($attributes);
312 $customForm['form_fields'] = json_decode($customForm['form_fields'], true);
313 $customForm['form_fields']['submitButton'] = $customForm['form']['submitButton'];
314 $customForm['form_fields'] = json_encode($customForm['form_fields']);
315 return $customForm;
316 }
317
318 protected function saveForm($formattedInputs, $title, $isStepForm = false, $isConversational = false, $customCss = '')
319 {
320 $customForm = $this->prepareCustomForm($formattedInputs, $isStepForm);
321 $data = Form::prepare($customForm);
322
323 $form = $this->model->create($data);
324 $form->title = $title ? $title : $form->title . ' (ChatGPT#' . $form->id . ')';
325
326 $formData = (object) $form->toArray();
327 if (FormFieldsParser::hasPaymentFields($formData)) {
328 $form->has_payment = 1;
329 }
330
331 if ($isConversational) {
332 $formMeta = FormMeta::prepare(['type' => 'form', 'predefined' => 'conversational'], $customForm);
333 $form->fill([
334 'form_fields' => Converter::convertExistingForm($form),
335 ])->save();
336 } else {
337 $form->save();
338 $formMeta = FormMeta::prepare(['type' => 'form', 'predefined' => 'blank_form'], $customForm);
339 }
340
341 FormMeta::store($form, $formMeta);
342
343 if ($customCss = fluentformSanitizeCSS($customCss)) {
344 Helper::setFormMeta($form->id, '_custom_form_css', $customCss);
345 }
346
347 do_action('fluentform/inserted_new_form', $form->id, $data);
348 return $form;
349 }
350
351 protected function prepareCustomForm($formattedInputs, $isStepForm)
352 {
353 $formattedInputs = fluentFormSanitizer($formattedInputs);
354 $customForm = $this->getBlankFormConfig();
355 $fields = json_decode($customForm['form_fields'], true);
356
357 $fields['form_fields']['fields'] = $formattedInputs;
358 $fields['form_fields']['submitButton'] = Arr::get($customForm, 'form.submitButton');
359
360 if ($isStepForm) {
361 $fields['form_fields']['stepsWrapper'] = $this->getStepWrapper();
362 }
363
364 $customForm['form_fields'] = json_encode($fields['form_fields']);
365
366 return $customForm;
367 }
368
369 protected function resolveContainerFields($field, $allFields)
370 {
371 $columns = Arr::get($field, 'columns');
372 $columnsCount = count($columns);
373 if (!$columnsCount || $columnsCount > 6) {
374 return [];
375 }
376 $matchedField = Arr::get($allFields, 'container.container_' . $columnsCount . '_col');
377 if (!$matchedField) {
378 return [];
379 }
380 $columnWidth = round(100 / $columnsCount, 2);
381 foreach ($columns as &$column) {
382 $formatedFields = [];
383 $fields = Arr::get($column, 'fields', []);
384 $columnWidth = Arr::get($column, 'width', $columnWidth);
385 foreach ($fields as $colField) {
386 $element = Arr::get($colField, 'element');
387 if ($columnField = $this->processField($element, $colField, $allFields)) {
388 $formatedFields[] = $columnField;
389 }
390 }
391 if ($formatedFields) {
392 $column['fields'] = $formatedFields;
393 $column['width'] = $columnWidth;
394 }
395 }
396 $matchedField['columns'] = $columns;
397 return $matchedField;
398 }
399
400 /**
401 * Build the step-wrapper skeleton used to wrap a multi-step form.
402 *
403 * @return array
404 */
405 protected function getStepWrapper()
406 {
407 return [
408 'stepStart' => [
409 'element' => 'step_start',
410 'attributes' => [
411 'id' => '',
412 'class' => '',
413 ],
414 'settings' => [
415 'progress_indicator' => 'progress-bar',
416 'step_titles' => [],
417 'disable_auto_focus' => 'no',
418 'enable_auto_slider' => 'no',
419 'enable_step_data_persistency' => 'no',
420 'enable_step_page_resume' => 'no',
421 ],
422 'editor_options' => [
423 'title' => 'Start Paging',
424 ],
425 ],
426 'stepEnd' => [
427 'element' => 'step_end',
428 'attributes' => [
429 'id' => '',
430 'class' => '',
431 ],
432 'settings' => [
433 'prev_btn' => [
434 'type' => 'default',
435 'text' => 'Previous',
436 'img_url' => '',
437 ],
438 ],
439 'editor_options' => [
440 'title' => 'End Paging',
441 ],
442 ],
443 ];
444 }
445
446 private function getUserPrompt($args)
447 {
448 $startingQuery = 'Create a form for ';
449 $query = Sanitizer::sanitizeTextField(Arr::get($args, 'query'));
450 if (empty($query)) {
451 throw new Exception(esc_html__('Query is empty!', 'fluentform'));
452 }
453
454 // Validate query length to prevent abuse (filterable; default 12000 characters)
455 $maxQueryLength = (int) apply_filters('fluentform/ai_query_max_length', 12000);
456 if (mb_strlen($query) > $maxQueryLength) {
457 throw new Exception(esc_html(sprintf(
458 /* translators: %d is the maximum allowed number of characters */
459 __('Query is too long. Please limit your prompt to %d characters.', 'fluentform'),
460 $maxQueryLength
461 )));
462 }
463
464 $additionalQuery = Sanitizer::sanitizeTextField(Arr::get($args, 'additional_query'));
465
466 // Validate additional query length (filterable; default 6000 characters)
467 $maxAdditionalQueryLength = (int) apply_filters('fluentform/ai_additional_query_max_length', 6000);
468 if ($additionalQuery && mb_strlen($additionalQuery) > $maxAdditionalQueryLength) {
469 throw new Exception(esc_html(sprintf(
470 /* translators: %d is the maximum allowed number of characters */
471 __('Additional query is too long. Please limit to %d characters.', 'fluentform'),
472 $maxAdditionalQueryLength
473 )));
474 }
475
476 if ($additionalQuery) {
477 $query .= "\n including questions for information like " . $additionalQuery . '.';
478 }
479 return $startingQuery . $query . $this->getPromptContractInstructions();
480 }
481
482 private function getPromptContractInstructions()
483 {
484 return "\n\nReturn strict JSON only. The user's instructions may be written in any language. "
485 . "Preserve labels and help text in the user's original language, but always return machine-readable field settings. "
486 . 'For every field, explicitly set whether it is required in settings.validation_rules.required.value when the prompt marks it as required or optional. '
487 . 'If the prompt lists allowed upload extensions, map them into settings.validation_rules.allowed_file_types.value. '
488 . 'If the prompt asks for a multi-step form with sections, include form_step elements between sections.';
489 }
490
491 private function applyPromptHints(array $form, array $args)
492 {
493 $query = (string) Arr::get($args, 'query', '');
494 $additionalQuery = (string) Arr::get($args, 'additional_query', '');
495 $hints = $this->extractPromptFieldHints(trim($query . "\n" . $additionalQuery));
496
497 if (!$hints) {
498 return $form;
499 }
500
501 $hintIndex = 0;
502 $form['fields'] = $this->applyHintsToFields(Arr::get($form, 'fields', []), $hints, $hintIndex);
503
504 return $form;
505 }
506
507 private function applyHintsToFields(array $fields, array $hints, &$hintIndex)
508 {
509 foreach ($fields as &$field) {
510 if (!is_array($field)) {
511 continue;
512 }
513
514 if (count($field) === 1) {
515 $field = reset($field);
516 }
517
518 $element = Arr::get($field, 'element');
519
520 if ('container' === $element) {
521 $columns = Arr::get($field, 'columns', []);
522 foreach ($columns as &$column) {
523 $column['fields'] = $this->applyHintsToFields(Arr::get($column, 'fields', []), $hints, $hintIndex);
524 }
525 $field['columns'] = $columns;
526 continue;
527 }
528
529 if (in_array($element, ['form_step', 'section_break'])) {
530 continue;
531 }
532
533 $hint = Arr::get($hints, $hintIndex);
534 ++$hintIndex;
535
536 if (!$hint) {
537 continue;
538 }
539
540 $field = $this->mergeFieldHint($field, $hint);
541 }
542
543 return $fields;
544 }
545
546 private function mergeFieldHint(array $field, array $hint)
547 {
548 if (array_key_exists('required', $hint)) {
549 Arr::set($field, 'settings.validation_rules.required.value', (bool) $hint['required']);
550 }
551
552 if (!empty($hint['help_message'])) {
553 Arr::set($field, 'settings.help_message', $hint['help_message']);
554 }
555
556 if (!empty($hint['allowed_file_types']) && in_array(Arr::get($field, 'element'), ['input_file', 'input_image'])) {
557 Arr::set($field, 'settings.validation_rules.allowed_file_types.value', array_values(array_unique($hint['allowed_file_types'])));
558 }
559
560 return $field;
561 }
562
563 private function extractPromptFieldHints($prompt)
564 {
565 if (!$prompt) {
566 return [];
567 }
568
569 $lines = preg_split('/\R/u', $prompt);
570 $hints = [];
571
572 foreach ($lines as $line) {
573 $line = trim(wp_strip_all_tags($line));
574 $line = preg_replace('/^[\-\*\d\.\)\s]+/u', '', $line);
575
576 if (!$line || !$this->looksLikeFieldLine($line)) {
577 continue;
578 }
579
580 $attributesText = '';
581 if (preg_match('/\(([^()]*)\)/u', $line, $matches)) {
582 $attributesText = trim($matches[1]);
583 }
584
585 $hint = [];
586 $normalizedLine = function_exists('mb_strtolower') ? mb_strtolower($line, 'UTF-8') : strtolower($line);
587
588 if ($this->containsAnyPhrase($normalizedLine, $this->getRequiredKeywords())) {
589 $hint['required'] = true;
590 } elseif ($this->containsAnyPhrase($normalizedLine, $this->getOptionalKeywords())) {
591 $hint['required'] = false;
592 }
593
594 $helpMessage = $this->extractHelpMessage($attributesText);
595 if ($helpMessage) {
596 $hint['help_message'] = $helpMessage;
597 }
598
599 if ($this->containsAnyPhrase($normalizedLine, $this->getFileUploadKeywords())) {
600 $allowedTypes = $this->extractAllowedFileTypes($line);
601 if ($allowedTypes) {
602 $hint['allowed_file_types'] = $allowedTypes;
603 }
604 }
605
606 $hints[] = $hint;
607 }
608
609 return $hints;
610 }
611
612 private function looksLikeFieldLine($line)
613 {
614 if (preg_match('/^(create|erstelle|crée|crear|criar|crea|maak|utw[]rz|oluştur|创建|作成)\b/ui', $line)) {
615 return false;
616 }
617
618 if (preg_match('/^(section|abschnitt|secci[]n|sectione|seção|sectie|sekcja|b[]l[]m|章节|セクション)\b/ui', $line)) {
619 return false;
620 }
621
622 if (false === strpos($line, '(')) {
623 return false;
624 }
625
626 preg_match('/\(([^()]*)\)/u', $line, $matches);
627 $attributesText = isset($matches[1]) ? $matches[1] : '';
628
629 return $this->containsAnyPhrase(
630 function_exists('mb_strtolower') ? mb_strtolower($attributesText, 'UTF-8') : strtolower($attributesText),
631 $this->getFieldDescriptorKeywords()
632 );
633 }
634
635 private function extractHelpMessage($attributesText)
636 {
637 if (!$attributesText) {
638 return '';
639 }
640
641 if (preg_match('/(?:note|hint|hinweis|remarque|nota|nota bene|opmerking|uwaga|not|说明|備考)\s*:\s*["“]?(.+?)["”]?$/ui', $attributesText, $matches)) {
642 return trim($matches[1], " \t\n\r\0\x0B\"'“”");
643 }
644
645 return '';
646 }
647
648 private function extractAllowedFileTypes($line)
649 {
650 preg_match_all('/\b(jpe?g|gif|png|tiff?|bmp|webp|svg|pdf|docx?|xlsx?|xls|csv|zip|rar|txt)\b/ui', $line, $matches);
651
652 if (empty($matches[1])) {
653 return [];
654 }
655
656 $types = array_map(function ($type) {
657 $type = strtolower($type);
658
659 if ('jpeg' === $type) {
660 return 'jpg';
661 }
662
663 if ('tif' === $type) {
664 return 'tiff';
665 }
666
667 return $type;
668 }, $matches[1]);
669
670 return array_values(array_unique($types));
671 }
672
673 private function containsAnyPhrase($text, array $phrases)
674 {
675 foreach ($phrases as $phrase) {
676 if (false !== strpos($text, $phrase)) {
677 return true;
678 }
679 }
680
681 return false;
682 }
683
684 private function getRequiredKeywords()
685 {
686 return [
687 'required',
688 'mandatory',
689 'pflichtfeld',
690 'erforderlich',
691 'obligatoire',
692 'requis',
693 'obligatorio',
694 'obrigatório',
695 'obbligatorio',
696 'verplicht',
697 'wymagane',
698 'zorunlu',
699 '�
700 ',
701 '�
702 ',
703 ];
704 }
705
706 private function getOptionalKeywords()
707 {
708 return [
709 'optional',
710 'facultatif',
711 'facoltativo',
712 'opcjonalne',
713 'isteğe bağlı',
714 'opcional',
715 'opcionales',
716 '选填',
717 '任意',
718 ];
719 }
720
721 private function getFileUploadKeywords()
722 {
723 return [
724 'file upload',
725 'upload',
726 'datei',
727 'datei upload',
728 'foto upload',
729 'image upload',
730 'upload de fichier',
731 'carga de archivo',
732 'subida de archivo',
733 'carregamento de arquivo',
734 'caricamento file',
735 'bestandsupload',
736 'yükleme',
737 '上传',
738 ];
739 }
740
741 private function getFieldDescriptorKeywords()
742 {
743 return [
744 'text',
745 'textarea',
746 'email',
747 'date',
748 'file',
749 'upload',
750 'phone',
751 'number',
752 'radio',
753 'checkbox',
754 'select',
755 'dropdown',
756 'image',
757 'foto',
758 'bild',
759 'datei',
760 'telefon',
761 'mobil',
762 'correo',
763 'texte',
764 'texto',
765 'testo',
766 'fecha',
767 'fichier',
768 'archivo',
769 'caricamento',
770 'ficheiro',
771 'bestand',
772 'yükleme',
773 '上传',
774 ];
775 }
776 }
777