In xslt 1.0, it looks pretty clumsy for those uninitiated.
[1] The basic replace workhorse of a single replacement (such as Northeast by NE) is a recursive named template. It is the named template [blue]r_sab[/blue] below. As it is quite instructive, and hence it is flooded with similar construction, some claim undue credit. I make this "credit-neutral", and don't claim any credit, form and have a little personal touch not seen elsewhere.
[2] And then the multiple replacement is again put into another named template ([blue]do-replace[/blue]) of no particular general interest.
Here would be the general constructions for this kind of task.
[tt]
<xsl:template name="do-replace">
<xsl

aram name="input" />
<xsl:variable name="w">
<xsl:call-template name="r_sab">
<xsl:with-param name="s" select="$input" />
<xsl:with-param name="a" select="'Northwest'" />
<xsl:with-param name="b" select="'NW'" />
</xsl:call-template>
</xsl:variable>
<xsl:variable name="x">
<xsl:call-template name="r_sab">
<xsl:with-param name="s" select="$w" />
<xsl:with-param name="a" select="'Northeast'" />
<xsl:with-param name="b" select="'NE'" />
</xsl:call-template>
</xsl:variable>
<xsl:variable name="y">
<xsl:call-template name="r_sab">
<xsl:with-param name="s" select="$x" />
<xsl:with-param name="a" select="'Southwest'" />
<xsl:with-param name="b" select="'SW'" />
</xsl:call-template>
</xsl:variable>
<xsl:variable name="z">
<xsl:call-template name="r_sab">
<xsl:with-param name="s" select="$y" />
<xsl:with-param name="a" select="'Southeast'" />
<xsl:with-param name="b" select="'SE'" />
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$z" />
</xsl:template>
<xsl:template name="r_sab">
<!-- replace substring common named template: credit neutral -->
<!--
param $s : input string
param $a : substring particle to be replace (globally)
param $b : substring particle to replace $a (globally)
-->
<xsl

aram name="s"/>
<xsl

aram name="a"/>
<xsl

aram name="b"/>
<xsl:choose>
<xsl:when test="contains($s, $a) and not($b = $a)">
<xsl:variable name="_a" select="substring-before($s, $a)" />
<xsl:variable name="a_" select="substring-after($s, $a)" />
<!-- packing result step-by-step -->
<xsl:value-of select="concat($_a,$b)" />
<xsl:call-template name="r_sab">
<xsl:with-param name="s" select="$a_" />
<xsl:with-param name="a" select="$a" />
<xsl:with-param name="b" select="$b" />
</xsl:call-template>
</xsl:when>
<xsl

therwise>
<!-- packing the last bit of substring -->
<xsl:value-of select="$s" />
</xsl

therwise>
</xsl:choose>
</xsl:template>
[/tt]
[3] When in the proper context node, you trigger the replacement mechanism like this.
[tt]
[red]<!--[/red]
<xsl:value-of select="current_observation/wind_string"/> [red]-->[/red]
[blue]<xsl:variable name="output">
<xsl:call-template name="do-replace">
<xsl:with-param name="input" select="current_observation/wind_string/text()" />
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$output" />[/blue]
[/tt]