You need to call a named recursive template for this. Try:
<xsl:template name="escape-quotes">
<xsl:param name="text"/>
<xsl:param name="searchString">'</xsl:param>
<xsl:param name="replaceString">&apos;</xsl:param>
<xsl:variable name="apos">'</xsl:variable>
<xsl:choose>
<xsl:when test="contains($text,$searchString)">
<xsl:call-template name="escape-quotes">
<xsl:with-param name="text" select="concat(substring-before($text,$searchString), $replaceString, substring-after($text,$searchString))"/>
<xsl:with-param name="searchString" select="$searchString"/>
<xsl:with-param name="replaceString" select="$replaceString"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="$searchString=$apos">
<xsl:call-template name="escape-quotes">
<xsl:with-param name="text" select="$text"/>
<xsl:with-param name="searchString">"</xsl:with-param>
<xsl:with-param name="replaceString">&quot;</xsl:with-param>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text" disable-output-escaping="yes"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Example of calling the template:
<output>
<xsl:call-template name="escape-quotes">
<xsl:with-param name="text">This is an "example" string. I have 'single quotes'.</xsl:with-param>
</xsl:call-template>
</output>
Result:
<output>This is an "example" string. I have 'single quotes'.</output>
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…