适用版本:FreeMarker 2.3+
核心机制:基于 Java 的 ResourceBundle + <#ftl> 指令 + ?i18n 内建函数
目标:实现模板层面的语言切换,支持中文、英文、日文等多语言


一、核心概念

概念 说明
<#ftl locale="zh_CN"> 在模板中声明默认语言环境(Locale),影响后续 ?i18n 行为
?i18n 内建函数 用于从 .properties 文件中获取对应语言的文本
ResourceBundle Java 标准的国际化资源加载机制,FreeMarker 基于此实现 i18n
messages_zh_CN.properties 中文资源文件(UTF-8 编码)
messages_en_US.properties 英文资源文件
?locale 获取当前模板或数据模型中的 Locale 设置

📌 关键理解

  • FreeMarker 本身不“翻译”,而是通过 ?i18n 读取外部 .properties 文件中的键值对。
  • <#ftl locale="..."> 设置模板的默认 Locale,可被运行时传入的 Locale 覆盖。

二、操作步骤(超详细,适合快速上手)

步骤 1:准备多语言资源文件(.properties

src/main/resources/i18n/ 目录下创建:

文件 1:messages.properties(默认语言,如英文)

# Default (en_US)
welcome=Welcome
login=Login
logout=Logout
language=Language
title=My Website
error.required=This field is required.

文件 2:messages_zh_CN.properties(简体中文)

# zh_CN
welcome=欢迎
login=登录
logout=退出
language=语言
title=我的网站
error.required=该字段为必填项。

文件 3:messages_ja_JP.properties(日文)

# ja_JP
welcome=ようこそ
login=ログイン
logout=ログアウト
language=言語
title=私のウェブサイト
error.required=このフィールドは必須です。

✅ 注意:

  • 文件名必须为 basename + _ + language + _ + country(如 messages_zh_CN
  • 所有文件必须使用 UTF-8 编码(建议用 IDE 设置或转码工具)
  • 键(key)保持一致,值(value)翻译不同

步骤 2:配置 FreeMarker Configuration 支持 i18n

import freemarker.template.Configuration;
import java.util.Locale;

@Configuration
public class FreeMarkerConfig {

    @Bean
    public Configuration freemarkerConfiguration() {
        Configuration config = new Configuration(Configuration.VERSION_2_3_31);
        config.setClassForTemplateLoading(getClass(), "/templates");
        config.setDefaultEncoding("UTF-8");
        // 设置默认 Locale(可选)
        config.setLocale(Locale.CHINA); // 默认 zh_CN
        return config;
    }
}

🔑 说明:

  • setLocale() 设置全局默认语言
  • 实际语言可在请求时动态传入(见步骤 4)

步骤 3:在模板中使用 <#ftl locale="...">?i18n

示例模板:home.ftl

<#ftl locale="zh_CN"> <!-- 声明模板默认语言,可被运行时覆盖 -->

<!DOCTYPE html>
<html lang="${currentLocale?replace('_', '-')}">
<head>
    <meta charset="UTF-8">
    <title>${"title"?i18n}</title>
</head>
<body>
    <h1>${"welcome"?i18n}</h1>
    <p>
        <a href="?lang=en_US">${"language"?i18n}: English</a> |
        <a href="?lang=zh_CN">${"language"?i18n}: 中文</a> |
        <a href="?lang=ja_JP">${"language"?i18n}: 日本語</a>
    </p>

    <form>
        <label>${"username"?i18n}:</label>
        <input type="text" name="username">
        <span class="error">${"error.required"?i18n}</span>
        <br>
        <button type="submit">${"login"?i18n}</button>
    </form>

    <footer>${"copyright"?i18n}</footer>
</body>
</html>

✅ 关键语法:

  • "key"?i18n:从资源文件中查找对应语言的文本
  • <#ftl locale="zh_CN">:设置模板默认语言(可省略,由 Java 传入决定)

步骤 4:在 Java 后端动态设置语言(Spring Boot 示例)

@Controller
public class I18nController {

    @Autowired
    private Configuration freemarkerConfig;

    @GetMapping("/")
    public String index(
            @RequestParam(value = "lang", required = false) String lang,
            Model model,
            HttpServletRequest request) throws IOException, TemplateException {

        // 1. 解析 Locale
        Locale locale = Locale.CHINA; // 默认中文
        if (lang != null && !lang.isEmpty()) {
            String[] parts = lang.split("_");
            if (parts.length == 2) {
                locale = new Locale(parts[0], parts[1]);
            }
        }

        // 2. 将 locale 传入模板
        model.addAttribute("currentLocale", locale.toString());

        // 3. 渲染模板(FreeMarker 会自动使用传入的 locale 进行 ?i18n 查找)
        Template template = freemarkerConfig.getTemplate("home.ftl");
        StringWriter out = new StringWriter();
        model.addAttribute("locale", locale); // 关键:传入 locale
        template.process(model.asMap(), out);

        model.addAttribute("content", out.toString());
        return "base";
    }
}

🔑 说明:

  • FreeMarker 在执行 "key"?i18n 时,会自动使用当前上下文的 Locale
  • 优先级:model.get("locale") > <#ftl locale="..."> > Configuration.setLocale()

三、常见错误与解决方案

错误现象 原因 解决方案
Missing bundle for xxx 资源文件未找到或命名错误 检查文件名、路径、拼写(如 messages_zh_CN.properties
显示 key 而非翻译文本(如 welcome 键不存在或文件编码错误 确认 .properties 文件中包含该 key,使用 UTF-8 保存
中文乱码 .properties 文件非 UTF-8 使用 IDE 转码或 native2ascii 工具处理
<#ftl locale="zh_CN"> 不生效 被 Java 传入的 locale 覆盖 检查是否在 model 中传入了 locale,或移除它
?i18n 报错 FreeMarker 版本过低 升级到 2.3.20+

四、注意事项

  1. .properties 文件必须 UTF-8 编码:Java 默认用 ISO-8859-1,FreeMarker 需显式支持 UTF-8。
  2. 资源文件路径必须在 classpath 中:如 src/main/resources/i18n/
  3. <#ftl locale="..."> 是模板级默认值,可被运行时传入的 locale 覆盖。
  4. ?i18n 只查找 basename 相同的资源:需在 Configuration 中设置 setI18nBasename("i18n/messages")
  5. 避免在 block 中使用 ?i18n 导致继承问题:确保父/子模板语言一致。

五、使用技巧

1. 设置全局资源基名(推荐)

config.setI18nBasename("i18n/messages"); // 自动加载 i18n/messages_zh_CN.properties

无需在模板中指定路径,?i18n 自动查找。

2. 使用 ${text?i18n} 动态 key

<#assign field = "username">
<label>${(field + ".label")?i18n}:</label> <!-- 转换为 "username.label"?i18n -->

3. 提供 fallback 机制(安全)

${("button.save"?i18n)! "Save"} <!-- 如果 key 不存在,显示默认值 -->

4. 在宏中使用 i18n

<#macro inputField name>
    <label>${(name + ".label")?i18n}:</label>
    <input type="text" name="${name}">
</#macro>

<@inputField name="username" />

六、最佳实践

实践 说明
✅ 统一资源文件命名 使用 messages_{lang}_{country}.properties
✅ 集中管理 keys 建立 key 文档,避免重复或拼写错误
✅ 使用英文作为默认语言文件 messages.properties 作为 fallback
✅ 避免在模板中硬编码文本 所有文本都应通过 ?i18n 获取
✅ 支持浏览器 Accept-Language 自动检测用户语言偏好
✅ 提供语言切换 UI 如顶部语言选择器

七、性能优化建议

  1. 启用资源缓存(默认开启):

    config.setLocalizedLookup(true); // 启用本地化查找缓存
    
  2. 避免频繁加载资源文件:FreeMarker 会缓存 ResourceBundle,无需手动管理。

  3. 减少 ?i18n 调用次数:对频繁使用的文本,可提前放入 model:

    model.addAttribute("btnLogin", "login".i18n(locale)); // Java 层翻译
    
  4. 使用 CDN 或静态化:对多语言页面,可生成静态 HTML 缓存。

  5. 异步加载语言包(高级):在 SPA 场景下,可前端动态加载 JSON 语言包。


八、总结:快速掌握要点

要点 一句话总结
资源文件 messages_zh_CN.properties,UTF-8 编码
<#ftl locale="..."> 设置模板默认语言(可被覆盖)
?i18n .properties 中获取翻译文本
setI18nBasename() 指定资源文件基路径
model.addAttribute("locale", ...) 运行时动态切换语言
编码问题 所有 .properties 必须 UTF-8

一句话掌握精髓

资源文件定翻译,?i18n 取文本,<#ftl> 设默认,Java 传 locale 真切换。


通过以上完整流程,你已具备在 FreeMarker 项目中实现多语言支持的能力。建议动手实践:创建 messages_zh_CN.propertiesmessages_en_US.properties,配合 <#ftl locale="zh_CN">?i18n,快速验证效果。