在strings.xml中使用占位符来存储运行时值,这种做法可行吗?

652

string.xml 中,是否可以使用占位符来表示字符串值,在运行时将其赋值?

例如:

一些字符串 PLACEHOLDER1 更多的字符串


可能是在strings.xml中引用另一个字符串的参考?的重复问题。 - Harish Gyanani
@HarishGyanani 不,这个比较旧,那个应该合并到这个里面。 - Danh
14个回答

1261

格式和样式

是的,可以参考来自字符串资源:格式和样式的以下内容。

如果你需要使用String.format(String, Object...)格式化字符串,则可以通过将格式化参数放入字符串资源中来实现。例如,使用以下资源:

<string name="welcome_messages">Hello, %1$s! You have %2$d new messages.</string>
在这个示例中,格式字符串有两个参数:%1$s是一个字符串,%2$d是一个十进制数。你可以这样从你的应用程序中格式化带有参数的字符串:
Resources res = getResources();
String text = String.format(res.getString(R.string.welcome_messages), username, mailCount);

基本用法

getString有一个重载,可以使用字符串作为格式字符串:

String text = res.getString(R.string.welcome_messages, username, mailCount);

复数形式

如果你需要处理复数形式,可以使用以下方法:

<plurals name="welcome_messages">
    <item quantity="one">Hello, %1$s! You have a new message.</item>
    <item quantity="other">Hello, %1$s! You have %2$d new messages.</item>
</plurals>

第一个mailCount参数用于确定使用哪种格式(单数或复数),其他参数是您的替换内容:

Resources res = getResources();
String text = res.getQuantityString(R.plurals.welcome_messages, mailCount, username, mailCount);

查看String Resources: Plurals了解更多细节。


72
第一个代码示例中的String.format调用实际上是不必要的,Resources.getString()支持格式化,参见:http://developer.android.com/reference/android/content/res/Resources.html#getString(int, java.lang.Object...) - Arnaud
17
在 String.xml 中,对于复数形式,您需要将 id 设为 R.plurals.welcome_messages,而不是 R.string.welcome_messages。 - om252345
复数事物只返回最后一项字符串...有什么办法可以获取附加字符串? - CoDe
1
当使用此方法格式化字符串时,它会删除您可能在 XML 字符串文本中使用的任何元标记,例如超链接等。 - Jono
5
字符串代表%1$s,十进制代表%2$d,整数代表什么?%1和%2的含义是什么?这是参数计数吗?如果我想要第三个参数,是将其命名为%3吗?答:整数代表"%3$d"。"%1$s"代表一个字符串,"%2$d"代表一个十进制数。"%1"和"%2"是参数的占位符,可以被替换为实际的值。这种语法用于格式化文本,并称为格式化字符串。如果您想使用第三个参数,则将其命名为"%3"。 - reegan29
显示剩余6条评论

421

补充回答

当我第一次看到已接受的答案中的%1$s%2$d时,它们毫无意义。这里有更多的解释。

它们被称为格式说明符。在XML字符串中,它们的形式为

%[parameter_index$][format_type] 
  • %: 百分号标记了格式说明符的开始。

  • 参数索引: 这是一个数字,后面跟着一个美元符号。如果您有三个参数要插入到字符串中,则它们将被称为1$2$3$。您在资源字符串中放置它们的顺序并不重要,只有您提供参数的顺序才重要。

  • 格式类型: 有很多种格式方式(请参见文档)。以下是一些常见的格式方式:

  • s 字符串

  • d 十进制整数

  • f 浮点数

示例

我们将创建以下格式化字符串,其中灰色部分是以编程方式插入的。

我的姐姐Mary今年12岁。

string.xml

<string name="my_xml_string">My sister %1$s is %2$d years old.</string>

我的活动.java

String myString = "Mary";
int myInt = 12;
String formatted = getString(R.string.my_xml_string, myString, myInt);

注意事项

  • 如果您在活动中,可以使用getString。 如果不可用,可以使用context.getResources().getString(...)
  • String.format()也可以格式化字符串。
  • 1$2$这些术语的顺序不需要按那个顺序使用。也就是说,2$可以出现在1$之前。当为使用不同单词顺序的语言本地化应用程序时,这很有用。
  • 如果要重复使用格式说明符例如%1$s,可以在xml中多次使用它。
  • 使用%%来获取实际的%字符。
  • 有关详细信息,请阅读以下有用的教程:Android SDK快速提示:格式化资源字符串

你知道那个美元符号的作用吗?我注意到有时候,即使没有它,我的应用程序也能正常工作。 例如: 在strings.xml中: <string name="symptoms_append">%1s 症状</string> 在我的活动中: setToolbarTitle(getString(R.string.symptoms_append, it)) - programmer dreamer
1
@programmerdreamer,抱歉,我已经有一段时间没有处理这个问题了,如果我曾经知道$的作用,现在我已经忘记了。 - Suragch

139

如果你想要使用来自实际 strings.xml 文件中的参数,而无需使用任何 Java 代码:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE resources [
  <!ENTITY appname "WhereDat">
  <!ENTITY author "Oded">
]>

<resources>
    <string name="app_name">&appname;</string>
    <string name="description">The &appname; app was created by &author;</string>
</resources>

这种方法不适用于资源文件,即必须将变量复制到每个需要它们的 XML 文件中。


这是一个很好的答案,除非你在 <![CDATA[ ¶m; ]]> 中有一个参数。 - Hermann Poilpre
2
有没有办法在单独的文件中定义 <!DOCTYPE... ]> 部分并将其包含在多个资源文件中?有什么技巧可以实现这一点吗? - prom85

21

我在寻找同样的内容,最终找到了以下非常简单的解决方案。最好的是:它可以直接使用。
1. 修改您的字符串资源:

<string name="welcome_messages">Hello, <xliff:g name="name">%s</xliff:g>! You have 
<xliff:g name="count">%d</xliff:g> new messages.</string>

2. 使用字符串替换:

c.getString(R.string.welcome_messages,name,count);

其中 c 是 Context,name 是一个字符串变量,count 是一个整型变量

你需要包含:

<resources xmlns:xliff="http://schemas.android.com/apk/res-auto">

在您的res/strings.xml文件中。
对我来说有效。


5
xliff 标签在格式说明符周围的作用是什么?与仅使用 %s%d 说明符相比,它们提供了什么额外的价值? - Richard Le Mesurier
在xliff标签中的"name"属性只是为了给翻译人员提供一个提示,告诉他们将会被替换的内容。有时候很难猜出"%s"代表什么意思。 - hugomg
"xliff:g" 用于通知翻译人员不应更改的部分。例如名称、优惠码,当然还有像上面那样的动态占位符。更多信息请参见 https://developer.android.com/guide/topics/resources/localization#mark-message-parts - Jeremy Fox
我们在这里使用相同的方法,效果非常好,除了一件事:我们通过Google Play使用的所有翻译器(也许是与Google Play本身有关的原因)都会在xliff:g标签之前和之后添加额外的行,这会在我们的字符串中创建额外的空格,并且使得翻译集成变得极其烦人。 - mtrewartha

9

如果你想要写百分号(%),需要将其复制一次:

<string name="percent">%1$d%%</string>

label.text = getString(R.string.percent, 75) // Output: 75%.

如果您简单地写成%1$d%,您将会得到错误:Format string 'percent' is not a valid format string so it should not be passed to String.format
或者使用formatted=false"

5
在Kotlin中,您只需要像这样设置字符串值:
<string name="song_number_and_title">"%1$d ~ %2$s"</string>

在您的布局中创建一个文本视图:
<TextView android:text="@string/song_number_and_title"/>

如果你正在使用Anko,请按照以下步骤在代码中执行:
val song = database.use { // get your song from the database }
song_number_and_title.setText(resources.getString(R.string.song_number_and_title, song.number, song.title))  

您可能需要从应用程序上下文中获取资源。

8
那与Java示例有何不同?我看不出区别。 - Janusz Hain

3

多语言项目

作为一名曾经参与过一个带有多种语言和配置变体的大型白标解决方案的人,我可以说这需要考虑很多因素。 除了文本方向之外,仅仅语法就足以让你头疼不已。 例如,项目中的项目顺序是否会发生变化,这也是需要考虑的。

<string name="welcome_messages">Hello, %1$s! You have %2$d new messages.</string>

应该优先选择

<string name="welcome_messages">Hello, %s! You have %d new messages.</string>

但是,一旦你与经常不知道什么是字符串或整数,更不用说每种类型使用什么格式字符的翻译人员合作,或者对于那些不知道代码中参数应用顺序的普通人,甚至是你自己忘记了,或者事情发生了变化,必须同时在多个地方更新,因此像MessageFormat这样使用。

<string name="welcome_message">Hello, {0}! You have {1} new messages.</string>

MessageFormat(R.string.welcome_message).format("Name", numMessages)

如果使用不可行,而且让非技术人员尝试理解 xlift 的想法甚至都不能被接受,那么我目前知道的最好的解决方案是使用显式、命名的占位符,如下所示:

<string name="placeholder_data" translatable="false">DATA</string>
<string name="placeholder_data" translatable="false">$DATA</string>
<string name="placeholder_data" translatable="false">%DATA%</string>

......或者其他不会与文本冲突的内容。

而您可以使用DOCTYPE,例如

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE resources [
  <!ENTITY placeholder_data "$DATA">
]>
<string name="text_with_data">Your data is &placeholder_data;.</string>

这将无法使用每种语言单独的文件来实现。
因此,在您的main/res/values/strings.xml中,像这样提供占位符和默认字符串。
<resources>

    <string name="placeholder_data" translatable="false">$DATA</string>
    <string name="placeholder_error" translatable="false">$ERROR</string>

    <string name="app_name">The App</string>

    <string name="content_loading">loading..</string>
    <string name="content_success">success: $DATA</string>
    <string name="content_error">error: $ERROR</string>

</resources>

然后在你的变体variant/res/values-de/strings.xml中进行设置

<resources>

    <string name="app_name">Die Applikation</string>

    <string name="content_loading">Ladevorgang..</string>
    <string name="content_success">Erfolg: $DATA</string>
    <string name="content_error">Netzwerkkommunikationsfehler: $ERROR</string>

</resources>

要使用它,可以写出类似以下的内容:

    textView.text = when (response) {
        is Data -> getText(content_success).resolveData(response.data)
        is Error -> getText(content_error).resolveError(response.error)
        is Loading -> getText(content_loading)
    }

使用一些辅助函数,例如:
    fun CharSequence.resolveData(data: JsonObject) =
        toString().replace(getString(placeholder_data), data.toString())

    fun CharSequence.resolveError(error: Throwable) =
        toString().replace(getString(placeholder_error), error.toString())

为了提供翻译文件和开发参考,每个构建风格不应该有默认文件。只需要一个单一的默认文件,然后是每种语言x变体的文件。

现在还有数字语法的问题。这可以通过使用plurals来解决,但这会增加xml文件的复杂性。正如指出的那样,zero并不能按照人们的预期工作。但是,您可能希望由于显示大小限制或UI预渲染图像数量的限制而对应用程序计数进行限制,并且需要显示99+而不是100。解决方法是使用一个辅助函数,例如:

    fun Context.getText(
        quantity: Int,
        @PluralsRes resIdQuantity: Int,
        @StringRes resIdNone: Int? = null,
        @StringRes resIdMoreThan: Int? = null,
        maxQuantity: Int? = null,
    ): CharSequence {
        if (resIdMoreThan != null && maxQuantity != null && quantity > maxQuantity)
            return getText(resIdMoreThan)
        return if (resIdNone != null && quantity == 0) return getText(resIdNone)
        else resources.getQuantityText(resIdQuantity, quantity)
    }

覆盖并扩展复数解析器的行为。

如果您有每个变体的可选功能,则添加一个res/values/strings-beans.xml,如下所示:

<resources>

    <string name="placeholder_name" translatable="false">$NAME</string>
    <string name="placeholder_count" translatable="false">$COUNT</string>

    <string name="beans_content_bean_count_zero">Hello $NAME! You have no beans.</string>
    <string name="beans_content_bean_count_one">Hello $NAME! You have one bean.</string>
    <string name="beans_content_bean_count_many">Hello $NAME! You have $COUNT beans.</string>
    <string name="beans_content_bean_count_more_than_9000">Hello $NAME! You have over $COUNT beans!</string>
    <string name="beans_content_bean_count_two">@string/beans_content_bean_count_many</string>
    <string name="beans_content_bean_count_few">@string/beans_content_bean_count_many</string>
    <string name="beans_content_bean_count_other">@string/beans_content_bean_count_many</string>
    <plurals name="beans_content_bean_count">
        <item quantity="zero">@string/beans_content_bean_count_zero</item>
        <item quantity="one">@string/beans_content_bean_count_one</item>
        <item quantity="two">@string/beans_content_bean_count_two</item>
        <item quantity="few">@string/beans_content_bean_count_few</item>
        <item quantity="many">@string/beans_content_bean_count_many</item>
        <item quantity="other">@string/beans_content_bean_count_other</item>
    </plurals>

</resources>

而在variant-with-beans/res/value-en/strings-beans.xml中的变量只需要包含以下内容:

<resources>

    <string name="beans_content_bean_count_zero">Hello $NAME! You have no beans.</string>
    <string name="beans_content_bean_count_one">Hello $NAME! You have one bean.</string>
    <string name="beans_content_bean_count_many">Hello $NAME! You have $COUNT beans.</string>
    <string name="beans_content_bean_count_more_than_9000">Hello $NAME! You have over 9000 beans!</string>

</resources>

并且可以在每个文件的基础上提供特定于语言的覆盖。为了使用它,代码可以如下所示:

    val name = "Bob"
    val beanCount = 3
    val limit = 9000
    text = getText(
        beanCount,
        beans_content_bean_count,
        beans_content_bean_count_zero,
        beans_content_bean_count_more_than_9000,
        limit,
    )
        .resolveCount(beanCount)
        .resolveName(name)

这将解析为输出结果

    beanCount = 0 -> "Hello Bob! You have no beans."
    beanCount = 1 -> "Hello Bob! You have one bean."
    beanCount = 3 -> "Hello Bob! You have 3 beans."
    beanCount = 9001 -> "Hello Bob! You have over 9000 beans!"

由于语言特定资源文件的简单性,它们可以通过部署工具从电子表格或公司自己的服务器端点等生成。希望您喜欢我的疯狂之旅,进入Android动态字符串资源的世界,并且希望您能够欣赏到,您不需要像我一样在iOS产品的开发中实现相同的功能,因为据我的经验,这需要使用Python脚本修改.xcodeproj文件并生成Swift代码。

3
在您的字符串文件中使用以下内容:
<string name="redeem_point"> You currently have %s points(%s points = 1 %s)</string>

在你的代码中相应地使用它

coinsTextTV.setText(String.format(getContext().getString(R.string.redeem_point), rewardPoints.getReward_points()
                        , rewardPoints.getConversion_rate(), getString(R.string.rs)));

2

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接