Skip to content

Localization in Notification Templates

User Guide: Using Localized Fields in Notification Templates (FreeMarker)

Overview

The HRM system provides automatic data localization based on each user’s preference settings (language, timezone, date/time format, number format). When configuring notification templates using FreeMarker, you can leverage both original and localized versions of data fields to create personalized, locale-aware notifications.

Understanding Localized Fields

Field Naming Convention

For every data field that contains localizable data (dates, times, numbers), the system automatically generates a localized version with the suffix _localized:

Original Field: ${fieldName}
Localized Field: ${fieldName_localized}

Example Data Structure

{
"employee": {
"name": "Nguyen Van A",
"joinDate": "2024-01-15T08:00:00+07:00",
"joinDate_localized": {
"full": "15/01/2024 08:00:00",
"dateOnly": "15/01/2024",
"timeOnly": "08:00:00",
"short": "15/01/2024 08:00",
"iso": "2024-01-15T08:00:00",
"relative": "3 tháng trước",
"timestamp": "1705284000000"
},
"salary": 15000000,
"salary_localized": "15.000.000",
"lastLogin": "2024-11-07T09:30:00+07:00",
"lastLogin_localized": {
"full": "07/11/2024 09:30:00",
"dateOnly": "07/11/2024",
"timeOnly": "09:30:00",
"short": "07/11/2024 09:30",
"iso": "2024-11-07T09:30:00",
"relative": "2 giờ trước",
"timestamp": "1730948400000"
}
}
}

Localizable Data Types

1. Date and Time Fields

IMPORTANT: All date/time fields are automatically localized using the MULTIPLE format representation. This means every date/time field gets converted into an object containing all possible format representations.

Available Date/Time Format Properties:

PropertyDescriptionExample (Vietnamese)Example (English)
fullComplete date and time15/01/2024 14:30:0001/15/2024 02:30:00 PM
dateOnlyDate without time15/01/202401/15/2024
timeOnlyTime without date14:30:0002:30:00 PM
shortDate and time without seconds15/01/2024 14:3001/15/2024 02:30 PM
isoISO 8601 format2024-01-15T14:30:002024-01-15T14:30:00
relativeHuman-friendly relative time2 giờ trước2 hours ago
timestampUnix timestamp in milliseconds17053062000001705306200000

Using Localized Date/Time in FreeMarker Templates:

<#-- Email Template Example -->
<p>Kính gửi ${employee.name},</p>
<#-- Use full format for complete information -->
<p>Ngày bắt đầu làm việc: ${employee.joinDate_localized.full}</p>
<#-- Output: Ngày bắt đầu làm việc: 15/01/2024 08:00:00 -->
<#-- Use relative format for recent activities -->
<p>Lần đăng nhập cuối: ${employee.lastLogin_localized.relative}</p>
<#-- Output: Lần đăng nhập cuối: 2 giờ trước -->
<#-- Use dateOnly for date display without time -->
<p>Ngày sinh: ${employee.birthDate_localized.dateOnly}</p>
<#-- Output: Ngày sinh: 15/01/1990 -->
<#-- Use short format for compact display -->
<p>Cập nhật: ${document.modifiedDate_localized.short}</p>
<#-- Output: Cập nhật: 07/11/2024 09:30 -->

Format Selection Guidelines:

  • full: Use for official documents, reports, audit logs
  • dateOnly: Use for birthdays, contract dates, deadline dates
  • timeOnly: Use for schedules, time entries, clock-in/out
  • short: Use for list views, compact displays
  • iso: Use for system integration, data export
  • relative: Use for dashboards, activity feeds, notifications
  • timestamp: Use for technical/system purposes

2. Number and Currency Fields

Numbers are automatically formatted according to user’s locale and number format preference:

<#-- Salary Display -->
<p>Lương cơ bản: ${employee.salary_localized} VND</p>
<#-- Vietnamese user: Lương cơ bản: 15.000.000 VND -->
<#-- English user: Lương cơ bản: 15,000,000 VND -->
<#-- Bonus Amount -->
<p>Thưởng tháng: ${bonus.amount_localized} ${currency}</p>
<#-- Working Hours -->
<p>Số giờ làm việc: ${timesheet.totalHours_localized} giờ</p>

3. Complex Nested Objects

Localization works recursively for nested objects and arrays:

{
"application": {
"submittedDate": "2024-11-01T10:00:00Z",
"submittedDate_localized": {
"full": "01/11/2024 17:00:00",
"dateOnly": "01/11/2024",
"timeOnly": "17:00:00",
"short": "01/11/2024 17:00",
"iso": "2024-11-01T17:00:00",
"relative": "6 ngày trước",
"timestamp": "1730462400000"
},
"candidate": {
"experience": 5.5,
"experience_localized": "5,5",
"interviews": [
{
"scheduledTime": "2024-11-10T09:00:00Z",
"scheduledTime_localized": {
"full": "10/11/2024 16:00:00",
"dateOnly": "10/11/2024",
"timeOnly": "16:00:00",
"short": "10/11/2024 16:00",
"iso": "2024-11-10T16:00:00",
"relative": "3 ngày nữa",
"timestamp": "1731247200000"
}
}
]
}
}
}
<#-- Accessing nested localized fields -->
<p>Ngày nộp hồ sơ: ${application.submittedDate_localized.dateOnly}</p>
<#-- Output: Ngày nộp hồ sơ: 01/11/2024 -->
<p>Kinh nghiệm: ${application.candidate.experience_localized} năm</p>
<#-- Output: Kinh nghiệm: 5,5 năm -->
<#-- Loop through interviews -->
<#list application.candidate.interviews as interview>
<p>Phỏng vấn lần ${interview?index + 1}: ${interview.scheduledTime_localized.full}</p>
<p>Còn lại: ${interview.scheduledTime_localized.relative}</p>
</#list>

Template Configuration Best Practices

1. Use Appropriate Date Format for Context

<#-- ✅ GOOD: Use relative time for recent activities -->
<p>Hoạt động gần nhất: ${activity.timestamp_localized.relative}</p>
<#-- Output: "5 phút trước" -->
<#-- ✅ GOOD: Use full format for official documents -->
<p>Ngày ký hợp đồng: ${contract.signedDate_localized.full}</p>
<#-- Output: "15/01/2024 14:30:00" -->
<#-- ✅ GOOD: Use dateOnly for simple date display -->
<p>Ngày sinh: ${employee.birthDate_localized.dateOnly}</p>
<#-- Output: "15/01/1990" -->
<#-- ❌ AVOID: Using ISO format for end users -->
<p>Ngày tạo: ${document.createdDate_localized.iso}</p>
<#-- Output: "2024-01-15T14:30:00" (not user-friendly) -->

2. Keep Original Fields for System Processing

<#-- Hidden fields for system tracking (use ISO timestamp) -->
<input type="hidden" name="submittedDate" value="${application.submittedDate_localized.timestamp}" />
<#-- Visible user-friendly display -->
<p>Ngày nộp: ${application.submittedDate_localized.full}</p>

3. Context-Aware Format Selection

<#-- Dashboard/Activity Feed: Use relative time -->
<div class="activity-feed">
<p>Nhiệm vụ được tạo ${task.createdDate_localized.relative}</p>
<#-- Output: "Nhiệm vụ được tạo 2 giờ trước" -->
</div>
<#-- Report/Document: Use full format -->
<div class="report">
<p>Ngày tạo báo cáo: ${report.createdDate_localized.full}</p>
<#-- Output: "Ngày tạo báo cáo: 15/01/2024 14:30:00" -->
</div>
<#-- Calendar/Schedule: Use short format -->
<div class="calendar-event">
<p>${event.startTime_localized.short} - ${event.endTime_localized.timeOnly}</p>
<#-- Output: "15/01/2024 14:30 - 16:00:00" -->
</div>
<#-- List View: Use dateOnly for compact display -->
<table>
<tr>
<td>${employee.name}</td>
<td>${employee.joinDate_localized.dateOnly}</td>
</tr>
</table>

4. Conditional Logic with FreeMarker

<#-- Check if data exists before accessing -->
<#if employee.joinDate_localized??>
<p>Ngày vào làm: ${employee.joinDate_localized.full}</p>
</#if>
<#-- Conditional formatting based on data -->
<#if employee.salary_localized??>
<p>Mức lương: ${employee.salary_localized} VND</p>
<#else>
<p>Mức lương: Chưa cập nhật</p>
</#if>
<#-- Use default values -->
<p>Cập nhật: ${document.modifiedDate_localized.relative!"Chưa có thông tin"}</p>

5. Multi-Language Support

<#-- Vietnamese Template -->
<p>Chào ${employee.name},</p>
<p>Ngày gia nhập: ${employee.joinDate_localized.full}</p>
<p>Cập nhật: ${employee.lastUpdate_localized.relative}</p>
<p>Lương: ${employee.salary_localized} VND</p>
<#-- English Template (same fields, different labels) -->
<p>Dear ${employee.name},</p>
<p>Join Date: ${employee.joinDate_localized.full}</p>
<p>Updated: ${employee.lastUpdate_localized.relative}</p>
<p>Salary: ${employee.salary_localized} VND</p>

Common Use Cases

1. Leave Request Notification

<h2>Thông báo đơn xin nghỉ phép</h2>
<p>Nhân viên: ${employee.name}</p>
<p>Ngày bắt đầu: ${leaveRequest.startDate_localized.dateOnly}</p>
<p>Ngày kết thúc: ${leaveRequest.endDate_localized.dateOnly}</p>
<p>Số ngày: ${leaveRequest.days_localized}</p>
<p>Thời gian nộp đơn: ${leaveRequest.submittedDate_localized.relative}</p>
<p>Vui lòng xem xét và phê duyệt.</p>

2. Timesheet Reminder

<h2>Nhắc nhở chấm công</h2>
<p>Kính gửi ${employee.name},</p>
<p>Bạn chưa chấm công cho tuần từ ${period.startDate_localized.dateOnly} đến ${period.endDate_localized.dateOnly}.</p>
<p>Tổng giờ làm việc hiện tại: ${timesheet.totalHours_localized} giờ</p>
<p>Hạn nộp: ${deadline_localized.full}</p>
<p>Thời gian còn lại: ${deadline_localized.relative}</p>
<p>Vui lòng hoàn thành chấm công trước thời hạn.</p>

3. Task Assignment

<h2>Công việc mới được giao</h2>
<p>Xin chào ${assignee.name},</p>
<p>Bạn được giao công việc: <strong>${task.name}</strong></p>
<p>Chi tiết:</p>
<ul>
<li>Ngày giao: ${task.assignedDate_localized.short}</li>
<li>Hạn hoàn thành: ${task.dueDate_localized.full}</li>
<li>Còn lại: ${task.dueDate_localized.relative}</li>
<li>Ưu tiên: ${task.priority}</li>
<li>Thời gian ước tính: ${task.estimatedHours_localized} giờ</li>
</ul>
<p>Vui lòng xác nhận và bắt đầu thực hiện.</p>

4. Recruitment Application Update

<h2>Cập nhật trạng thái ứng tuyển</h2>
<p>Kính gửi ${candidate.name},</p>
<p>Hồ sơ ứng tuyển của bạn đã được cập nhật:</p>
<p><strong>Vị trí:</strong> ${application.jobTitle}</p>
<p><strong>Ngày nộp:</strong> ${application.submittedDate_localized.dateOnly}</p>
<p><strong>Trạng thái:</strong> ${application.status}</p>
<p><strong>Thời gian xử lý:</strong> ${application.processedDate_localized.relative}</p>
<#if interview??>
<p><strong>Lịch phỏng vấn:</strong></p>
<ul>
<li>Thời gian: ${interview.scheduledTime_localized.full}</li>
<li>Còn: ${interview.scheduledTime_localized.relative}</li>
<li>Địa điểm: ${interview.location}</li>
<li>Người phỏng vấn: ${interview.interviewer}</li>
</ul>
</#if>
<p>Chúc bạn thành công!</p>

5. Activity Feed / Dashboard

<#-- Perfect use case for relative time -->
<div class="activity-feed">
<h3>Hoạt động gần đây</h3>
<#list activities as activity>
<div class="activity-item">
<p>${activity.description}</p>
<span class="timestamp">${activity.timestamp_localized.relative}</span>
<span class="date">${activity.timestamp_localized.short}</span>
</div>
</#list>
</div>
<#-- Output example:
Nhiệm vụ mới được tạo
vừa xong | 07/11/2024 09:30
Đơn nghỉ phép được phê duyệt
2 giờ trước | 07/11/2024 07:30
Chấm công tuần đã hoàn thành
hôm qua | 06/11/2024 17:00
-->

6. Advanced FreeMarker Features

<#-- Using macros for reusable components -->
<#macro displayDate date format="full">
<#if date??>
<#if format == "full">
${date.full}
<#elseif format == "short">
${date.short}
<#elseif format == "relative">
${date.relative}
<#else>
${date.dateOnly}
</#if>
<#else>
N/A
</#if>
</#macro>
<#-- Using the macro -->
<p>Ngày tạo: <@displayDate date=document.createdDate_localized format="full" /></p>
<p>Cập nhật: <@displayDate date=document.modifiedDate_localized format="relative" /></p>
<#-- Functions for complex logic -->
<#function formatAmount amount>
<#if amount??>
<#return amount + " VND">
<#else>
<#return "Chưa xác định">
</#if>
</#function>
<p>Tổng tiền: ${formatAmount(invoice.amount_localized)}</p>
<#-- Built-in string operations -->
<p>Tên viết hoa: ${employee.name?upper_case}</p>
<p>Email: ${employee.email?lower_case}</p>
<p>Số ký tự: ${employee.name?length}</p>
<#-- Number formatting -->
<#if employee.salary??>
<p>Lương (định dạng): ${employee.salary?string.currency}</p>
<p>Lương (phần trăm): ${(employee.bonusRate * 100)?string.number}%</p>
</#if>
<#-- Date comparisons -->
<#if (task.dueDate_localized.timestamp?number) < (.now?long)>
<p class="overdue">Nhiệm vụ đã quá hạn!</p>
<#else>
<p class="active">Còn thời gian: ${task.dueDate_localized.relative}</p>
</#if>

Timezone Conversion

The system automatically converts all timestamps to the user’s preferred timezone. The localized object contains the converted values:

<#-- Original timestamp in UTC: 2024-11-07T02:30:00Z -->
<#-- Vietnamese user (Asia/Ho_Chi_Minh, GMT+7): -->
<p>Thời gian meeting: ${meeting.startTime_localized.full}</p>
<#-- Output: 07/11/2024 09:30:00 -->
<#-- Japanese user (Asia/Tokyo, GMT+9): -->
<p>ミーティング時間: ${meeting.startTime_localized.full}</p>
<#-- Output: 2024/11/07 11:30:00 -->
<#-- All date formats in the _localized object are timezone-converted -->
<p>ISO: ${meeting.startTime_localized.iso}</p>
<p>Relative: ${meeting.startTime_localized.relative}</p>
<p>Short: ${meeting.startTime_localized.short}</p>

FreeMarker-Specific Tips

1. Null Safety

<#-- Use ?? to check if variable exists -->
<#if employee.joinDate_localized??>
<p>Ngày vào: ${employee.joinDate_localized.full}</p>
</#if>
<#-- Use ! for default values -->
<p>Ngày sinh: ${employee.birthDate_localized.dateOnly!"Chưa cập nhật"}</p>
<#-- Chain null checks -->
<p>Phòng ban: ${employee.department.name!"Chưa phân công"}</p>

2. Collections and Loops

<#-- Check if list exists and has items -->
<#if tasks?? && tasks?size gt 0>
<ul>
<#list tasks as task>
<li>${task?index + 1}. ${task.name} - ${task.dueDate_localized.relative}</li>
</#list>
</ul>
<#else>
<p>Không có nhiệm vụ nào</p>
</#if>
<#-- Loop with separator -->
<p>Nhân viên:
<#list employees as emp>
${emp.name}<#sep>, </#sep>
</#list>
</p>

3. Include and Import

<#-- Include common header -->
<#include "email-header.ftl">
<#-- Import macros from library -->
<#import "date-formatters.ftl" as dateLib>
<p>Formatted Date: <@dateLib.formatDate date=employee.joinDate_localized /></p>

4. Escaping

<#-- HTML escape by default -->
<p>${employee.notes}</p>
<#-- No escape for trusted HTML -->
<p>${employee.description?no_esc}</p>
<#-- URL encoding -->
<a href="/profile?name=${employee.name?url}">Profile</a>
<#-- JavaScript string escape -->
<script>
var name = '${employee.name?js_string}';
</script>

Troubleshooting

Issue: FreeMarker shows object representation

Cause: Forgetting to specify which format property to use

Solution: Always access a specific property from the localized object

<#-- ❌ WRONG: Will show [object Object] or hash representation -->
<p>Ngày: ${employee.joinDate_localized}</p>
<#-- ✅ CORRECT: Access specific format -->
<p>Ngày: ${employee.joinDate_localized.full}</p>
<p>Ngày: ${employee.joinDate_localized.dateOnly}</p>
<p>Ngày: ${employee.joinDate_localized.relative}</p>

Issue: Null pointer errors

Cause: Accessing properties on null/undefined values

Solution: Use FreeMarker’s null safety operators

<#-- ❌ WRONG: May cause error if joinDate_localized is null -->
<p>Ngày: ${employee.joinDate_localized.full}</p>
<#-- ✅ CORRECT: Check for null first -->
<#if employee.joinDate_localized??>
<p>Ngày: ${employee.joinDate_localized.full}</p>
</#if>
<#-- ✅ ALTERNATIVE: Use default value -->
<p>Ngày: ${employee.joinDate_localized.full!"Chưa xác định"}</p>

Issue: Date shows in wrong timezone

Cause: User preference timezone not set correctly

Solution: Verify user’s timezone preference in profile settings

Issue: Number format not matching expectation

Cause: Number format pattern in user preferences

Solution: Check user’s numberFormat preference (default: ###,###.##)

Summary

Quick Reference

Data TypeUse Localized?FreeMarker Access
Date/Time✅ Yes (MULTIPLE format)${startDate_localized.full}
Numbers✅ Yes${amount_localized}
Currency✅ Yes${salary_localized}
Strings❌ No${name}
Booleans❌ No${isActive?c}
Enums❌ No${status}

Date/Time Format Selection Guide

ContextRecommended FormatFreeMarker Example
Official Documentsfull${date_localized.full}
Reportsfull${date_localized.full}
DeadlinesdateOnly${deadline_localized.dateOnly}
Activity Feedrelative${activity_localized.relative}
Dashboardrelative or short${update_localized.relative}
Calendarshort${event_localized.short}
List ViewdateOnly or short${created_localized.dateOnly}
Time EntriestimeOnly${clockIn_localized.timeOnly}
System Integrationiso or timestamp${data_localized.timestamp}

Best Practices Checklist

  • ✅ Always access specific format property from _localized object
  • ✅ Use <#if variable??> to check for null before accessing
  • ✅ Use ${variable!"default"} for default values
  • ✅ Use relative format for recent activities and notifications
  • ✅ Use full format for official documents and reports
  • ✅ Use dateOnly for simple date displays (birthdays, deadlines)
  • ✅ Use short format for compact list views
  • ✅ Keep original fields for system processing (use timestamp or iso)
  • ✅ Test templates with different user preferences (language, timezone)
  • ✅ Use FreeMarker macros for reusable components
  • ✅ Implement proper null safety with ?? and ! operators
  • ✅ Consider multi-language support in template design

Note: The localization service automatically processes all date/time data using the MULTIPLE format representation. Template designers using FreeMarker can choose the most appropriate format for their specific use case by accessing the corresponding property from the _localized object using the ${variable.property} syntax.