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:
| Property | Description | Example (Vietnamese) | Example (English) |
|---|---|---|---|
| full | Complete date and time | 15/01/2024 14:30:00 | 01/15/2024 02:30:00 PM |
| dateOnly | Date without time | 15/01/2024 | 01/15/2024 |
| timeOnly | Time without date | 14:30:00 | 02:30:00 PM |
| short | Date and time without seconds | 15/01/2024 14:30 | 01/15/2024 02:30 PM |
| iso | ISO 8601 format | 2024-01-15T14:30:00 | 2024-01-15T14:30:00 |
| relative | Human-friendly relative time | 2 giờ trước | 2 hours ago |
| timestamp | Unix timestamp in milliseconds | 1705306200000 | 1705306200000 |
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 Type | Use 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
| Context | Recommended Format | FreeMarker Example |
|---|---|---|
| Official Documents | full | ${date_localized.full} |
| Reports | full | ${date_localized.full} |
| Deadlines | dateOnly | ${deadline_localized.dateOnly} |
| Activity Feed | relative | ${activity_localized.relative} |
| Dashboard | relative or short | ${update_localized.relative} |
| Calendar | short | ${event_localized.short} |
| List View | dateOnly or short | ${created_localized.dateOnly} |
| Time Entries | timeOnly | ${clockIn_localized.timeOnly} |
| System Integration | iso or timestamp | ${data_localized.timestamp} |
Best Practices Checklist
- ✅ Always access specific format property from
_localizedobject - ✅ Use
<#if variable??>to check for null before accessing - ✅ Use
${variable!"default"}for default values - ✅ Use
relativeformat for recent activities and notifications - ✅ Use
fullformat for official documents and reports - ✅ Use
dateOnlyfor simple date displays (birthdays, deadlines) - ✅ Use
shortformat for compact list views - ✅ Keep original fields for system processing (use
timestamporiso) - ✅ 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.