diff --git a/lib/core/utils/prompt_variable_parser.dart b/lib/core/utils/prompt_variable_parser.dart new file mode 100644 index 0000000..77e3e51 --- /dev/null +++ b/lib/core/utils/prompt_variable_parser.dart @@ -0,0 +1,429 @@ +import 'package:flutter/services.dart'; +import 'package:intl/intl.dart'; + +/// Represents a parsed prompt variable. +/// +/// Variables can be either system variables (auto-resolved) or custom input +/// variables (require user input). +/// +/// Syntax: `{{variable_name}}` or `{{variable_name | type:property=value}}` +class PromptVariable { + const PromptVariable({ + required this.fullMatch, + required this.name, + required this.type, + required this.properties, + required this.start, + required this.end, + }); + + /// The full matched string including `{{` and `}}`. + final String fullMatch; + + /// The variable name (e.g., "description", "CURRENT_DATE"). + final String name; + + /// The input type (e.g., "text", "textarea", "select", "number"). + /// Null for simple variables without type specification. + final PromptVariableType type; + + /// Additional properties like placeholder, default, required, options. + final Map properties; + + /// Start index of the match in the original string. + final int start; + + /// End index of the match in the original string. + final int end; + + /// Whether this variable requires user input. + bool get requiresUserInput => !isSystemVariable; + + /// Whether this is a system variable that can be auto-resolved. + bool get isSystemVariable { + final upper = name.toUpperCase(); + return _systemVariableNames.contains(upper); + } + + /// Whether this field is marked as required. + bool get isRequired => properties['required'] == 'true'; + + /// Get the placeholder text, if specified. + String? get placeholder => properties['placeholder']; + + /// Get the default value, if specified. + String? get defaultValue => properties['default']; + + /// Get min value for number inputs. + double? get min { + final val = properties['min']; + return val != null ? double.tryParse(val) : null; + } + + /// Get max value for number inputs. + double? get max { + final val = properties['max']; + return val != null ? double.tryParse(val) : null; + } + + /// Get step value for number inputs. + double? get step { + final val = properties['step']; + return val != null ? double.tryParse(val) : null; + } + + /// Get options for select inputs. + List get options { + final optionsStr = properties['options']; + if (optionsStr == null) return const []; + // Parse JSON array format: ["Option1","Option2"] + final trimmed = optionsStr.trim(); + if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return const []; + final inner = trimmed.substring(1, trimmed.length - 1); + if (inner.isEmpty) return const []; + return inner + .split(',') + .map((s) => s.trim()) + .map((s) { + // Remove surrounding quotes + if ((s.startsWith('"') && s.endsWith('"')) || + (s.startsWith("'") && s.endsWith("'"))) { + return s.substring(1, s.length - 1); + } + return s; + }) + .where((s) => s.isNotEmpty) + .toList(); + } + + /// Display label for the variable (formatted from name). + String get displayLabel { + // Convert snake_case or camelCase to Title Case + final words = name + .replaceAllMapped( + RegExp(r'([a-z])([A-Z])'), + (m) => '${m.group(1)} ${m.group(2)}', + ) + .replaceAll('_', ' ') + .split(' ') + .map( + (w) => w.isNotEmpty + ? '${w[0].toUpperCase()}${w.substring(1).toLowerCase()}' + : '', + ) + .join(' '); + return words; + } + + static const Set _systemVariableNames = { + 'CLIPBOARD', + 'CURRENT_DATE', + 'CURRENT_DATETIME', + 'CURRENT_TIME', + 'CURRENT_TIMEZONE', + 'CURRENT_WEEKDAY', + 'USER_NAME', + 'USER_LANGUAGE', + 'USER_LOCATION', + }; +} + +/// Types of prompt variable inputs. +enum PromptVariableType { + /// Simple text input (single line). + text, + + /// Multi-line text input. + textarea, + + /// Dropdown select. + select, + + /// Number input. + number, +} + +/// Parses prompt content to extract variables. +class PromptVariableParser { + const PromptVariableParser(); + + /// Regular expression to match prompt variables. + /// Matches: {{variable_name}} or {{variable_name | type:prop=value:prop2=value2}} + static final _variablePattern = RegExp( + r'\{\{([^{}|]+?)(?:\s*\|\s*([^{}]+?))?\}\}', + multiLine: true, + ); + + /// Parse all variables from prompt content. + List parse(String content) { + final variables = []; + final matches = _variablePattern.allMatches(content); + + for (final match in matches) { + final fullMatch = match.group(0)!; + final name = match.group(1)!.trim(); + final typeAndProps = match.group(2)?.trim(); + + var type = PromptVariableType.text; + final properties = {}; + + if (typeAndProps != null && typeAndProps.isNotEmpty) { + // Parse type and properties: type:prop1=value1:prop2=value2 + final parts = _parseTypeAndProperties(typeAndProps); + type = parts.type; + properties.addAll(parts.properties); + } + + variables.add( + PromptVariable( + fullMatch: fullMatch, + name: name, + type: type, + properties: properties, + start: match.start, + end: match.end, + ), + ); + } + + return variables; + } + + /// Parse type and properties from the part after `|`. + _TypeAndProperties _parseTypeAndProperties(String input) { + var type = PromptVariableType.text; + final properties = {}; + + // Split by `:` but handle nested structures like options=["a","b"] + final segments = _splitProperties(input); + + for (var i = 0; i < segments.length; i++) { + final segment = segments[i].trim(); + if (segment.isEmpty) continue; + + if (i == 0 && !segment.contains('=')) { + // First segment without = is the type + type = _parseType(segment); + } else if (segment.contains('=')) { + // Property=value pair + final eqIndex = segment.indexOf('='); + final key = segment.substring(0, eqIndex).trim().toLowerCase(); + final value = segment.substring(eqIndex + 1).trim(); + properties[key] = value; + } else if (segment.toLowerCase() == 'required') { + // Boolean flag + properties['required'] = 'true'; + } + } + + return _TypeAndProperties(type: type, properties: properties); + } + + /// Split properties while respecting brackets. + List _splitProperties(String input) { + final segments = []; + var current = StringBuffer(); + var bracketDepth = 0; + var inQuotes = false; + String? quoteChar; + + for (var i = 0; i < input.length; i++) { + final char = input[i]; + + if (inQuotes) { + current.write(char); + if (char == quoteChar && (i == 0 || input[i - 1] != r'\')) { + inQuotes = false; + quoteChar = null; + } + } else if (char == '"' || char == "'") { + inQuotes = true; + quoteChar = char; + current.write(char); + } else if (char == '[' || char == '{' || char == '(') { + bracketDepth++; + current.write(char); + } else if (char == ']' || char == '}' || char == ')') { + bracketDepth--; + current.write(char); + } else if (char == ':' && bracketDepth == 0) { + segments.add(current.toString()); + current = StringBuffer(); + } else { + current.write(char); + } + } + + if (current.isNotEmpty) { + segments.add(current.toString()); + } + + return segments; + } + + PromptVariableType _parseType(String typeStr) { + switch (typeStr.toLowerCase()) { + case 'textarea': + return PromptVariableType.textarea; + case 'select': + return PromptVariableType.select; + case 'number': + return PromptVariableType.number; + case 'text': + default: + return PromptVariableType.text; + } + } + + /// Check if content has any variables that require user input. + bool hasUserInputVariables(String content) { + final variables = parse(content); + return variables.any((v) => v.requiresUserInput); + } + + /// Check if content has any variables (system or user input). + bool hasVariables(String content) { + return _variablePattern.hasMatch(content); + } +} + +class _TypeAndProperties { + const _TypeAndProperties({required this.type, required this.properties}); + + final PromptVariableType type; + final Map properties; +} + +/// Resolves system variables to their actual values. +class SystemVariableResolver { + const SystemVariableResolver({ + this.userName, + this.userLanguage, + this.userLocation, + }); + + final String? userName; + final String? userLanguage; + final String? userLocation; + + /// Resolve a system variable to its value. + /// Returns null if the variable cannot be resolved. + Future resolve(String variableName) async { + final upper = variableName.toUpperCase(); + + switch (upper) { + case 'CLIPBOARD': + return _getClipboard(); + case 'CURRENT_DATE': + return DateFormat.yMMMd().format(DateTime.now()); + case 'CURRENT_DATETIME': + return DateFormat.yMMMd().add_jm().format(DateTime.now()); + case 'CURRENT_TIME': + return DateFormat.jm().format(DateTime.now()); + case 'CURRENT_TIMEZONE': + return DateTime.now().timeZoneName; + case 'CURRENT_WEEKDAY': + return DateFormat.EEEE().format(DateTime.now()); + case 'USER_NAME': + return userName ?? ''; + case 'USER_LANGUAGE': + return userLanguage ?? ''; + case 'USER_LOCATION': + return userLocation ?? ''; + default: + return null; + } + } + + Future _getClipboard() async { + try { + final data = await Clipboard.getData(Clipboard.kTextPlain); + return data?.text ?? ''; + } catch (_) { + return ''; + } + } +} + +/// Result of processing a prompt with variables. +class ProcessedPrompt { + const ProcessedPrompt({ + required this.content, + required this.userInputVariables, + }); + + /// The prompt content with system variables already resolved. + final String content; + + /// Variables that still require user input. + final List userInputVariables; + + /// Whether user input is still needed. + bool get needsUserInput => userInputVariables.isNotEmpty; +} + +/// Processes prompt content by resolving system variables and identifying +/// user input variables. +class PromptProcessor { + const PromptProcessor({required this.parser, required this.systemResolver}); + + final PromptVariableParser parser; + final SystemVariableResolver systemResolver; + + /// Process prompt content. + /// + /// Returns a [ProcessedPrompt] with system variables resolved and + /// a list of variables that need user input. + Future process(String content) async { + final variables = parser.parse(content); + if (variables.isEmpty) { + return ProcessedPrompt(content: content, userInputVariables: const []); + } + + var processedContent = content; + final userInputVars = []; + + // Process variables in reverse order to preserve indices + for (final variable in variables.reversed) { + if (variable.isSystemVariable) { + final resolved = await systemResolver.resolve(variable.name); + if (resolved != null) { + processedContent = processedContent.replaceRange( + variable.start, + variable.end, + resolved, + ); + } + } else { + userInputVars.insert(0, variable); + } + } + + return ProcessedPrompt( + content: processedContent, + userInputVariables: userInputVars, + ); + } + + /// Apply user-provided values to the processed content. + String applyUserValues(String content, Map values) { + final variables = parser.parse(content); + if (variables.isEmpty) return content; + + var result = content; + + // Apply in reverse order to preserve indices + for (final variable in variables.reversed) { + if (!variable.isSystemVariable && values.containsKey(variable.name)) { + result = result.replaceRange( + variable.start, + variable.end, + values[variable.name]!, + ); + } + } + + return result; + } +} + diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index b631000..8b5f9b6 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -29,6 +29,9 @@ import '../../../core/models/knowledge_base.dart'; import '../../../shared/utils/platform_utils.dart'; import 'package:conduit/l10n/app_localizations.dart'; import '../../../shared/widgets/modal_safe_area.dart'; +import '../../../core/utils/prompt_variable_parser.dart'; +import '../../prompts/widgets/prompt_variable_dialog.dart'; +import '../../auth/providers/unified_auth_providers.dart'; class _SendMessageIntent extends Intent { const _SendMessageIntent(); @@ -468,10 +471,80 @@ class _ModernChatInputState extends ConsumerState final TextRange? range = _currentPromptRange; if (range == null) return; + // Check if the prompt has variables that need processing + const parser = PromptVariableParser(); + if (parser.hasVariables(prompt.content)) { + _processPromptWithVariables(prompt, range); + } else { + _insertPromptContent(prompt.content, range); + } + } + + Future _processPromptWithVariables( + Prompt prompt, + TextRange range, + ) async { + // Hide overlay first + setState(() { + _showPromptOverlay = false; + _currentPromptCommand = ''; + _currentPromptRange = null; + _promptSelectionIndex = 0; + }); + + // Get user info for system variables + final authUser = ref.read(currentUserProvider2); + final userAsync = ref.read(currentUserProvider); + final user = userAsync.maybeWhen( + data: (value) => value ?? authUser, + orElse: () => authUser, + ); + final locale = Localizations.localeOf(context); + + // Create the processor with system variable context + const parser = PromptVariableParser(); + final systemResolver = SystemVariableResolver( + userName: user?.name ?? user?.email, + userLanguage: locale.languageCode, + // userLocation requires permission - left empty for now + ); + final processor = PromptProcessor( + parser: parser, + systemResolver: systemResolver, + ); + + // Process system variables first + final processed = await processor.process(prompt.content); + if (!mounted) return; + + String finalContent = processed.content; + + // If there are user input variables, show the dialog + if (processed.needsUserInput) { + final values = await PromptVariableDialog.show( + context, + variables: processed.userInputVariables, + promptTitle: prompt.title, + ); + + if (values == null || !mounted) { + // User cancelled - restore focus + _ensureFocusedIfEnabled(); + return; + } + + // Apply user-provided values + finalContent = processor.applyUserValues(finalContent, values); + } + + // Insert the fully processed content + _insertPromptContent(finalContent, range); + } + + void _insertPromptContent(String content, TextRange range) { final String text = _controller.text; final String before = text.substring(0, range.start); final String after = text.substring(range.end); - final String content = prompt.content; final int caret = before.length + content.length; _controller.value = TextEditingValue( diff --git a/lib/features/prompts/widgets/prompt_variable_dialog.dart b/lib/features/prompts/widgets/prompt_variable_dialog.dart new file mode 100644 index 0000000..f3d868c --- /dev/null +++ b/lib/features/prompts/widgets/prompt_variable_dialog.dart @@ -0,0 +1,351 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'package:conduit/core/utils/prompt_variable_parser.dart'; +import 'package:conduit/l10n/app_localizations.dart'; +import 'package:conduit/shared/theme/theme_extensions.dart'; + +// Use AppTypography constants for font sizes + +/// A dialog that collects user input for prompt variables. +/// +/// Displays input fields for each variable based on their type (text, textarea, +/// select, number) and validates required fields before submission. +class PromptVariableDialog extends StatefulWidget { + const PromptVariableDialog({ + super.key, + required this.variables, + required this.promptTitle, + }); + + /// The variables that require user input. + final List variables; + + /// The title of the prompt being used. + final String promptTitle; + + /// Shows the dialog and returns a map of variable name to user-provided value. + /// Returns null if the user cancels the dialog. + static Future?> show( + BuildContext context, { + required List variables, + required String promptTitle, + }) { + return showDialog>( + context: context, + barrierDismissible: false, + builder: (ctx) => + PromptVariableDialog(variables: variables, promptTitle: promptTitle), + ); + } + + @override + State createState() => _PromptVariableDialogState(); +} + +class _PromptVariableDialogState extends State { + late final Map _controllers; + late final Map _selectValues; + final _formKey = GlobalKey(); + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + _controllers = {}; + _selectValues = {}; + + for (final variable in widget.variables) { + if (variable.type == PromptVariableType.select) { + _selectValues[variable.name] = variable.defaultValue; + } else { + _controllers[variable.name] = TextEditingController( + text: variable.defaultValue ?? '', + ); + } + } + } + + @override + void dispose() { + for (final controller in _controllers.values) { + controller.dispose(); + } + super.dispose(); + } + + void _submit() { + if (_isSubmitting) return; + + if (!_formKey.currentState!.validate()) { + return; + } + + setState(() => _isSubmitting = true); + + final values = {}; + for (final variable in widget.variables) { + if (variable.type == PromptVariableType.select) { + values[variable.name] = _selectValues[variable.name] ?? ''; + } else { + values[variable.name] = _controllers[variable.name]?.text ?? ''; + } + } + + Navigator.of(context).pop(values); + } + + @override + Widget build(BuildContext context) { + final theme = context.conduitTheme; + final l10n = AppLocalizations.of(context)!; + + return AlertDialog( + backgroundColor: theme.surfaceBackground, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.dialog), + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.promptTitle.isNotEmpty + ? widget.promptTitle + : l10n.promptVariablesTitle, + style: TextStyle( + color: theme.textPrimary, + fontSize: AppTypography.bodyLarge, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: Spacing.xs), + Text( + l10n.promptVariablesDescription, + style: TextStyle( + color: theme.textSecondary, + fontSize: AppTypography.bodySmall, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + content: SizedBox( + width: double.maxFinite, + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (var i = 0; i < widget.variables.length; i++) ...[ + if (i > 0) const SizedBox(height: Spacing.md), + _buildField(widget.variables[i]), + ], + ], + ), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(null), + child: Text( + l10n.cancel, + style: TextStyle(color: theme.textSecondary), + ), + ), + TextButton( + onPressed: _isSubmitting ? null : _submit, + child: Text( + l10n.continueAction, + style: TextStyle( + color: _isSubmitting ? theme.textSecondary : theme.buttonPrimary, + ), + ), + ), + ], + ); + } + + Widget _buildField(PromptVariable variable) { + final theme = context.conduitTheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + variable.displayLabel, + style: TextStyle( + color: theme.textPrimary, + fontSize: AppTypography.bodySmall, + fontWeight: FontWeight.w500, + ), + ), + ), + if (variable.isRequired) + Text( + ' *', + style: TextStyle( + color: theme.error, + fontSize: AppTypography.bodySmall, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + const SizedBox(height: Spacing.xs), + _buildInputWidget(variable), + ], + ); + } + + Widget _buildInputWidget(PromptVariable variable) { + switch (variable.type) { + case PromptVariableType.textarea: + return _buildTextareaField(variable); + case PromptVariableType.select: + return _buildSelectField(variable); + case PromptVariableType.number: + return _buildNumberField(variable); + case PromptVariableType.text: + return _buildTextField(variable); + } + } + + Widget _buildTextField(PromptVariable variable) { + final theme = context.conduitTheme; + final l10n = AppLocalizations.of(context)!; + + return TextFormField( + controller: _controllers[variable.name], + style: TextStyle(color: theme.inputText), + decoration: _inputDecoration(theme, variable.placeholder), + validator: (value) { + if (variable.isRequired && (value == null || value.trim().isEmpty)) { + return l10n.requiredFieldHelper; + } + return null; + }, + onFieldSubmitted: (_) => _submit(), + ); + } + + Widget _buildTextareaField(PromptVariable variable) { + final theme = context.conduitTheme; + final l10n = AppLocalizations.of(context)!; + + return TextFormField( + controller: _controllers[variable.name], + style: TextStyle(color: theme.inputText), + decoration: _inputDecoration(theme, variable.placeholder), + minLines: 3, + maxLines: 6, + validator: (value) { + if (variable.isRequired && (value == null || value.trim().isEmpty)) { + return l10n.requiredFieldHelper; + } + return null; + }, + ); + } + + Widget _buildSelectField(PromptVariable variable) { + final theme = context.conduitTheme; + final l10n = AppLocalizations.of(context)!; + final options = variable.options; + + return DropdownButtonFormField( + initialValue: _selectValues[variable.name], + decoration: _inputDecoration(theme, variable.placeholder), + dropdownColor: theme.surfaceBackground, + style: TextStyle(color: theme.inputText), + items: options.map((option) { + return DropdownMenuItem(value: option, child: Text(option)); + }).toList(), + onChanged: (value) { + setState(() { + _selectValues[variable.name] = value; + }); + }, + validator: (value) { + if (variable.isRequired && (value == null || value.isEmpty)) { + return l10n.requiredFieldHelper; + } + return null; + }, + ); + } + + Widget _buildNumberField(PromptVariable variable) { + final theme = context.conduitTheme; + final l10n = AppLocalizations.of(context)!; + + return TextFormField( + controller: _controllers[variable.name], + style: TextStyle(color: theme.inputText), + decoration: _inputDecoration(theme, variable.placeholder), + keyboardType: TextInputType.numberWithOptions( + decimal: variable.step != null && variable.step! < 1, + signed: variable.min != null && variable.min! < 0, + ), + inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'[\d.-]'))], + validator: (value) { + if (variable.isRequired && (value == null || value.trim().isEmpty)) { + return l10n.requiredFieldHelper; + } + if (value != null && value.isNotEmpty) { + final num = double.tryParse(value); + if (num == null) { + return l10n.validationFormatError; + } + if (variable.min != null && num < variable.min!) { + return l10n.promptVariableNumberMin(variable.min!); + } + if (variable.max != null && num > variable.max!) { + return l10n.promptVariableNumberMax(variable.max!); + } + } + return null; + }, + onFieldSubmitted: (_) => _submit(), + ); + } + + InputDecoration _inputDecoration(ConduitThemeExtension theme, String? hint) { + return InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: theme.inputPlaceholder), + filled: true, + fillColor: theme.inputBackground, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide(color: theme.inputBorder, width: 1), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide(color: theme.buttonPrimary, width: 1), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide(color: theme.error, width: 1), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + borderSide: BorderSide(color: theme.error, width: 1.5), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.md, + ), + ); + } +} + diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 304ab93..4d75ea0 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -807,5 +807,9 @@ "tapToExpand": "Tippen zum Erweitern", "byAuthor": "Von {name}", "wordCount": "{count} Wörter", - "charCount": "{count} Zeichen" + "charCount": "{count} Zeichen", + "promptVariablesTitle": "Details ausfüllen", + "promptVariablesDescription": "Füllen Sie die folgenden Felder aus, um diese Vorlage anzupassen.", + "promptVariableNumberMin": "Mindestwert ist {min}", + "promptVariableNumberMax": "Höchstwert ist {max}" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index de624cd..80674a6 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1794,5 +1794,33 @@ "chartPreviewUnavailable": "Chart preview is not available on this platform.", "@chartPreviewUnavailable": { "description": "Shown when Chart.js charts cannot be rendered on the current platform." + }, + "promptVariablesTitle": "Fill in Details", + "@promptVariablesTitle": { + "description": "Default title for the prompt variables dialog." + }, + "promptVariablesDescription": "Complete the fields below to customize this prompt.", + "@promptVariablesDescription": { + "description": "Description shown in the prompt variables dialog." + }, + "promptVariableNumberMin": "Minimum value is {min}", + "@promptVariableNumberMin": { + "description": "Validation message when a number is below the minimum.", + "placeholders": { + "min": { + "type": "double", + "example": "0" + } + } + }, + "promptVariableNumberMax": "Maximum value is {max}", + "@promptVariableNumberMax": { + "description": "Validation message when a number is above the maximum.", + "placeholders": { + "max": { + "type": "double", + "example": "100" + } + } } } diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 4529b4d..7e664e6 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -807,5 +807,9 @@ "tapToExpand": "Toca para expandir", "byAuthor": "Por {name}", "wordCount": "{count} palabras", - "charCount": "{count} caracteres" + "charCount": "{count} caracteres", + "promptVariablesTitle": "Completar detalles", + "promptVariablesDescription": "Complete los campos a continuación para personalizar este prompt.", + "promptVariableNumberMin": "El valor mínimo es {min}", + "promptVariableNumberMax": "El valor máximo es {max}" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index b5c419c..c09407c 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -807,5 +807,9 @@ "tapToExpand": "Appuyez pour développer", "byAuthor": "Par {name}", "wordCount": "{count} mots", - "charCount": "{count} caractères" + "charCount": "{count} caractères", + "promptVariablesTitle": "Remplir les détails", + "promptVariablesDescription": "Complétez les champs ci-dessous pour personnaliser ce prompt.", + "promptVariableNumberMin": "La valeur minimale est {min}", + "promptVariableNumberMax": "La valeur maximale est {max}" } diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index d5f4dd4..9d91a5a 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -807,5 +807,9 @@ "tapToExpand": "Tocca per espandere", "byAuthor": "Di {name}", "wordCount": "{count} parole", - "charCount": "{count} caratteri" + "charCount": "{count} caratteri", + "promptVariablesTitle": "Compila i dettagli", + "promptVariablesDescription": "Compila i campi sottostanti per personalizzare questo prompt.", + "promptVariableNumberMin": "Il valore minimo è {min}", + "promptVariableNumberMax": "Il valore massimo è {max}" } diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 03c6340..39555a6 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -585,5 +585,9 @@ "tapToExpand": "탭하여 확장", "byAuthor": "{name} 작성", "wordCount": "{count}단어", - "charCount": "{count}자" + "charCount": "{count}자", + "promptVariablesTitle": "세부 정보 입력", + "promptVariablesDescription": "아래 필드를 작성하여 이 프롬프트를 사용자 정의하세요.", + "promptVariableNumberMin": "최소값은 {min}입니다", + "promptVariableNumberMax": "최대값은 {max}입니다" } diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index f2b1704..6e7a195 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -807,5 +807,9 @@ "tapToExpand": "Tik om uit te vouwen", "byAuthor": "Door {name}", "wordCount": "{count} woorden", - "charCount": "{count} tekens" + "charCount": "{count} tekens", + "promptVariablesTitle": "Details invullen", + "promptVariablesDescription": "Vul de onderstaande velden in om deze prompt aan te passen.", + "promptVariableNumberMin": "Minimale waarde is {min}", + "promptVariableNumberMax": "Maximale waarde is {max}" } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 90ee837..d3eab00 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -807,5 +807,9 @@ "tapToExpand": "Нажмите для раскрытия", "byAuthor": "Автор: {name}", "wordCount": "{count} слов", - "charCount": "{count} символов" + "charCount": "{count} символов", + "promptVariablesTitle": "Заполните данные", + "promptVariablesDescription": "Заполните поля ниже, чтобы настроить этот промпт.", + "promptVariableNumberMin": "Минимальное значение: {min}", + "promptVariableNumberMax": "Максимальное значение: {max}" } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 91da20f..65b90aa 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -807,5 +807,9 @@ "tapToExpand": "点击展开", "byAuthor": "作者:{name}", "wordCount": "{count} 字", - "charCount": "{count} 字符" + "charCount": "{count} 字符", + "promptVariablesTitle": "填写详情", + "promptVariablesDescription": "填写以下字段以自定义此提示词。", + "promptVariableNumberMin": "最小值为 {min}", + "promptVariableNumberMax": "最大值为 {max}" } diff --git a/lib/l10n/app_zh_Hant.arb b/lib/l10n/app_zh_Hant.arb index 31da857..b1330f5 100644 --- a/lib/l10n/app_zh_Hant.arb +++ b/lib/l10n/app_zh_Hant.arb @@ -807,5 +807,9 @@ "tapToExpand": "點擊展開", "byAuthor": "作者:{name}", "wordCount": "{count} 字", - "charCount": "{count} 字元" + "charCount": "{count} 字元", + "promptVariablesTitle": "填寫詳情", + "promptVariablesDescription": "填寫以下欄位以自訂此提示詞。", + "promptVariableNumberMin": "最小值為 {min}", + "promptVariableNumberMax": "最大值為 {max}" }