diff --git a/phpcs-sniffs/Formidable/Sniffs/Translations/UseSharedTranslationDomainSniff.php b/phpcs-sniffs/Formidable/Sniffs/Translations/UseSharedTranslationDomainSniff.php
new file mode 100644
index 0000000000..0adc9d26a6
--- /dev/null
+++ b/phpcs-sniffs/Formidable/Sniffs/Translations/UseSharedTranslationDomainSniff.php
@@ -0,0 +1,408 @@
+
+ *
+ *
+ *
+ *
+ *
+ * @var string
+ */
+ public $potFilePath = '';
+
+ /**
+ * The target text domain that should be used for shared strings.
+ *
+ * @var string
+ */
+ public $targetDomain = 'formidable';
+
+ /**
+ * The plugin-specific text domain for strings that don't exist in Lite.
+ *
+ * Can be configured in ruleset.xml:
+ *
+ *
+ *
+ *
+ *
+ *
+ * @var string
+ */
+ public $pluginDomain = '';
+
+ /**
+ * Cache of strings from the .pot file.
+ *
+ * @var array|null
+ */
+ private static $potStrings;
+
+ /**
+ * Translation functions to check.
+ *
+ * Key is function name, value is array with:
+ * - 'string' => argument position of the translatable string (0-indexed)
+ * - 'domain' => argument position of the text domain (0-indexed)
+ *
+ * @var array
+ */
+ private $translationFunctions = array(
+ '__' => array(
+ 'string' => 0,
+ 'domain' => 1,
+ ),
+ '_e' => array(
+ 'string' => 0,
+ 'domain' => 1,
+ ),
+ '_x' => array(
+ 'string' => 0,
+ 'domain' => 2,
+ ),
+ '_ex' => array(
+ 'string' => 0,
+ 'domain' => 2,
+ ),
+ '_n' => array(
+ 'string' => 0,
+ 'domain' => 4,
+ ),
+ '_nx' => array(
+ 'string' => 0,
+ 'domain' => 5,
+ ),
+ 'esc_html__' => array(
+ 'string' => 0,
+ 'domain' => 1,
+ ),
+ 'esc_html_e' => array(
+ 'string' => 0,
+ 'domain' => 1,
+ ),
+ 'esc_html_x' => array(
+ 'string' => 0,
+ 'domain' => 2,
+ ),
+ 'esc_attr__' => array(
+ 'string' => 0,
+ 'domain' => 1,
+ ),
+ 'esc_attr_e' => array(
+ 'string' => 0,
+ 'domain' => 1,
+ ),
+ 'esc_attr_x' => array(
+ 'string' => 0,
+ 'domain' => 2,
+ ),
+ '_n_noop' => array(
+ 'string' => 0,
+ 'domain' => 2,
+ ),
+ '_nx_noop' => array(
+ 'string' => 0,
+ 'domain' => 3,
+ ),
+ );
+
+ /**
+ * Returns an array of tokens this sniff wants to listen for.
+ *
+ * @return array
+ */
+ public function register() {
+ return array( T_STRING );
+ }
+
+ /**
+ * Processes this sniff when one of its tokens is encountered.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr The position of the current token in the stack.
+ *
+ * @return void
+ */
+ public function process( File $phpcsFile, $stackPtr ) {
+ $tokens = $phpcsFile->getTokens();
+
+ $functionName = $tokens[ $stackPtr ]['content'];
+
+ // Check if this is a translation function we care about.
+ if ( ! isset( $this->translationFunctions[ $functionName ] ) ) {
+ return;
+ }
+
+ // Make sure it's a function call.
+ $nextToken = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true );
+
+ if ( false === $nextToken || $tokens[ $nextToken ]['code'] !== T_OPEN_PARENTHESIS ) {
+ return;
+ }
+
+ // Load the .pot file if not already loaded.
+ if ( null === self::$potStrings ) {
+ self::$potStrings = $this->loadPotFile();
+ }
+
+ // If we couldn't load the .pot file, skip.
+ if ( empty( self::$potStrings ) ) {
+ return;
+ }
+
+ $functionConfig = $this->translationFunctions[ $functionName ];
+ $arguments = $this->getFunctionArguments( $phpcsFile, $nextToken );
+
+ // Check if we have enough arguments.
+ if ( count( $arguments ) <= $functionConfig['domain'] ) {
+ return;
+ }
+
+ $stringArg = $arguments[ $functionConfig['string'] ] ?? null;
+ $domainArg = $arguments[ $functionConfig['domain'] ] ?? null;
+
+ if ( null === $stringArg || null === $domainArg ) {
+ return;
+ }
+
+ // Get the string value.
+ $stringValue = $this->getStringValue( $phpcsFile, $stringArg );
+
+ if ( null === $stringValue ) {
+ return;
+ }
+
+ // Get the domain value.
+ $domainValue = $this->getStringValue( $phpcsFile, $domainArg );
+
+ if ( null === $domainValue ) {
+ return;
+ }
+
+ $stringExistsInLite = isset( self::$potStrings[ $stringValue ] );
+
+ // Case 1: String exists in Lite but uses wrong domain -> use 'formidable'.
+ if ( $stringExistsInLite && $domainValue !== $this->targetDomain ) {
+ $error = sprintf(
+ 'String "%s" exists in Lite plugin. Use text domain "%s" instead of "%s".',
+ strlen( $stringValue ) > 50 ? substr( $stringValue, 0, 47 ) . '...' : $stringValue,
+ $this->targetDomain,
+ $domainValue
+ );
+
+ $fix = $phpcsFile->addFixableError( $error, $domainArg['start'], 'UseSharedDomain' );
+
+ if ( $fix ) {
+ $this->fixTextDomain( $phpcsFile, $tokens, $domainArg, $this->targetDomain );
+ }
+
+ return;
+ }
+
+ // Case 2: String does NOT exist in Lite but uses 'formidable' -> use plugin domain.
+ if ( ! $stringExistsInLite && $domainValue === $this->targetDomain && ! empty( $this->pluginDomain ) ) {
+ $error = sprintf(
+ 'String "%s" does not exist in Lite plugin. Use text domain "%s" instead of "%s".',
+ strlen( $stringValue ) > 50 ? substr( $stringValue, 0, 47 ) . '...' : $stringValue,
+ $this->pluginDomain,
+ $domainValue
+ );
+
+ $fix = $phpcsFile->addFixableError( $error, $domainArg['start'], 'UsePluginDomain' );
+
+ if ( $fix ) {
+ $this->fixTextDomain( $phpcsFile, $tokens, $domainArg, $this->pluginDomain );
+ }
+ }
+ }
+
+ /**
+ * Fix the text domain in a translation call.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param array $tokens The tokens array.
+ * @param array{start: int, end: int} $domainArg The domain argument info.
+ * @param string $newDomain The new text domain to use.
+ *
+ * @return void
+ */
+ private function fixTextDomain( File $phpcsFile, array $tokens, array $domainArg, string $newDomain ) {
+ $phpcsFile->fixer->beginChangeset();
+
+ // Find the actual string token within the argument.
+ for ( $i = $domainArg['start']; $i <= $domainArg['end']; $i++ ) {
+ if ( $tokens[ $i ]['code'] === T_CONSTANT_ENCAPSED_STRING ) {
+ $quote = substr( $tokens[ $i ]['content'], 0, 1 );
+ $newContent = $quote . $newDomain . $quote;
+ $phpcsFile->fixer->replaceToken( $i, $newContent );
+ break;
+ }
+ }
+
+ $phpcsFile->fixer->endChangeset();
+ }
+
+ /**
+ * Load and parse the .pot file.
+ *
+ * @return array Array of msgid strings as keys.
+ */
+ private function loadPotFile() {
+ if ( empty( $this->potFilePath ) || ! file_exists( $this->potFilePath ) ) {
+ return array();
+ }
+
+ $content = file_get_contents( $this->potFilePath );
+
+ if ( false === $content ) {
+ return array();
+ }
+
+ $strings = array();
+
+ // Parse msgid entries from the .pot file.
+ // Match msgid "string" or msgid "" followed by continuation strings.
+ preg_match_all( '/^msgid\s+"(.*)"/m', $content, $matches );
+
+ if ( ! empty( $matches[1] ) ) {
+ foreach ( $matches[1] as $string ) {
+ // Skip empty strings (header).
+ if ( '' === $string ) {
+ continue;
+ }
+
+ // Unescape the string.
+ $string = $this->unescapePotString( $string );
+
+ $strings[ $string ] = true;
+ }
+ }
+
+ return $strings;
+ }
+
+ /**
+ * Unescape a string from .pot file format.
+ *
+ * @param string $string The escaped string.
+ *
+ * @return string The unescaped string.
+ */
+ private function unescapePotString( $string ) {
+ $string = str_replace( '\\n', "\n", $string );
+ $string = str_replace( '\\t', "\t", $string );
+ $string = str_replace( '\\"', '"', $string );
+ return str_replace( '\\\\', '\\', $string );
+ }
+
+ /**
+ * Get the arguments of a function call.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $openParen The position of the opening parenthesis.
+ *
+ * @return array Array of argument info.
+ */
+ private function getFunctionArguments( File $phpcsFile, $openParen ) {
+ $tokens = $phpcsFile->getTokens();
+
+ if ( ! isset( $tokens[ $openParen ]['parenthesis_closer'] ) ) {
+ return array();
+ }
+
+ $closeParen = $tokens[ $openParen ]['parenthesis_closer'];
+ $arguments = array();
+ $argStart = $openParen + 1;
+ $depth = 0;
+
+ for ( $i = $openParen + 1; $i < $closeParen; $i++ ) {
+ $code = $tokens[ $i ]['code'];
+
+ if ( $code === T_OPEN_PARENTHESIS || $code === T_OPEN_SQUARE_BRACKET || $code === T_OPEN_CURLY_BRACKET ) {
+ ++$depth;
+ } elseif ( $code === T_CLOSE_PARENTHESIS || $code === T_CLOSE_SQUARE_BRACKET || $code === T_CLOSE_CURLY_BRACKET ) {
+ --$depth;
+ } elseif ( $code === T_COMMA && 0 === $depth ) {
+ $arguments[] = array(
+ 'start' => $argStart,
+ 'end' => $i - 1,
+ );
+ $argStart = $i + 1;
+ }
+ }
+
+ // Add the last argument.
+ if ( $argStart < $closeParen ) {
+ $arguments[] = array(
+ 'start' => $argStart,
+ 'end' => $closeParen - 1,
+ );
+ }
+
+ return $arguments;
+ }
+
+ /**
+ * Get the string value from an argument.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param array{start: int, end: int} $arg The argument info.
+ *
+ * @return string|null The string value or null if not a simple string.
+ */
+ private function getStringValue( File $phpcsFile, $arg ) {
+ $tokens = $phpcsFile->getTokens();
+
+ // Find the first non-whitespace token in the argument.
+ $stringToken = null;
+
+ for ( $i = $arg['start']; $i <= $arg['end']; $i++ ) {
+ if ( $tokens[ $i ]['code'] === T_WHITESPACE ) {
+ continue;
+ }
+
+ if ( $tokens[ $i ]['code'] === T_CONSTANT_ENCAPSED_STRING ) {
+ $stringToken = $i;
+ break;
+ }
+
+ // Not a simple string literal.
+ return null;
+ }
+
+ if ( null === $stringToken ) {
+ return null;
+ }
+
+ $content = $tokens[ $stringToken ]['content'];
+
+ // Remove quotes.
+ $quote = substr( $content, 0, 1 );
+
+ if ( $quote === '"' || $quote === "'" ) {
+ $content = substr( $content, 1, -1 );
+ }
+
+ return $content;
+ }
+}