PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.11
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.11
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.11, at app/Modules/Ai/AiFormBuilder.php

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