Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
07c9acf
feat: implement locale-aware utilities in edge-apps-library
nicomiguelino Dec 24, 2025
dd725b9
refactor: improve locale validation and localized day names
nicomiguelino Dec 24, 2025
cc4d8b2
feat(edge-apps-library): format a date in a locale-aware way
nicomiguelino Dec 24, 2025
218ff0b
refactor: consolidate formatting utilities into locale.ts
nicomiguelino Dec 24, 2025
e957d2b
chore(edge-apps-library): make time zone test cases async
nicomiguelino Dec 24, 2025
9051486
test: add comprehensive locale utility tests
nicomiguelino Dec 24, 2025
c3ae600
docs: add locale utility functions to README
nicomiguelino Dec 24, 2025
482e045
test: rename redundant test case names
nicomiguelino Dec 24, 2025
53a4632
test: add override_timezone and fallback scenarios to getTimeZone tests
nicomiguelino Dec 24, 2025
e76ef96
test: add getLocale tests including multiple underscore normalization
nicomiguelino Dec 24, 2025
37e7cf4
feat: improve locale validation with stricter format checking
nicomiguelino Dec 24, 2025
f82e495
feat: use Intl.Locale API for robust numeral system handling
nicomiguelino Dec 24, 2025
6752666
refactor: parametrize locale-specific tests for concurrent execution
nicomiguelino Dec 24, 2025
68b1223
build: enable parallel test execution via --parallel flag
nicomiguelino Dec 24, 2025
1e4050e
Merge branch 'master' into nicomiguelino/fix-570
nicomiguelino Dec 24, 2025
0178d50
Merge branch 'master' into nicomiguelino/fix-570
nicomiguelino Dec 25, 2025
f6cf095
Merge branch 'master' into nicomiguelino/fix-570
nicomiguelino Dec 25, 2025
aeec9b0
Merge branch 'master' into nicomiguelino/fix-570
nicomiguelino Dec 25, 2025
216129c
Merge branch 'master' into nicomiguelino/fix-570
nicomiguelino Dec 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion edge-apps/edge-apps-library/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@ signalReady()
- `getMetadata()` - Get all screen metadata
- `getScreenName()`, `getHostname()`, `getLocation()`, `getHardware()`, `getScreenlyVersion()`, `getTags()`, `hasTag(tag)`, `getFormattedCoordinates()`

### Location
### Location & Localization

- `getTimeZone()` - Get timezone from GPS coordinates
- `getLocale()` - Get locale from location
- `formatCoordinates(coords)` - Format coordinates as string
- `formatLocalizedDate(date, locale)` - Format date for locale
- `formatTime(date, locale, timezone)` - Format time for locale
- `getLocalizedDayNames(locale)` - Get day names for locale
- `getLocalizedMonthNames(locale)` - Get month names for locale
- `detectHourFormat(locale)` - Detect 12h/24h format for locale

### UTM Tracking

Expand Down
6 changes: 3 additions & 3 deletions edge-apps/edge-apps-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@
"scripts": {
"build": "bun build:types",
"build:types": "tsc",
"test": "bun test",
"test:unit": "bun test",
"test:coverage": "bun test --coverage",
"test": "bun test --parallel",
"test:unit": "bun test --parallel",
"test:coverage": "bun test --coverage --parallel",
"lint": "tsc --noEmit",
"format": "prettier --write src/ README.md index.html",
"format:check": "prettier --check src/ README.md index.html"
Expand Down
54 changes: 54 additions & 0 deletions edge-apps/edge-apps-library/src/utils/detect-hour-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, test, expect } from 'bun:test'
import { detectHourFormat } from './locale'

describe('detectHourFormat', () => {
test('should return hour12 for US locales', () => {
const locales = ['en-US', 'en']
for (const locale of locales) {
const format = detectHourFormat(locale)
expect(format).toBe('hour12')
}
})

test('should return hour24 for most European locales', () => {
const locales = [
'en-GB',
'de-DE',
'de',
'fr-FR',
'fr',
'es-ES',
'es',
'ru-RU',
'ru',
]
for (const locale of locales) {
const format = detectHourFormat(locale)
expect(format).toBe('hour24')
}
})

test('should return hour24 for some Asian locales', () => {
const locales = ['ja-JP', 'ja', 'zh-CN', 'zh']
for (const locale of locales) {
const format = detectHourFormat(locale)
expect(format).toBe('hour24')
}
})

test('should return hour24 for Thailand locale', () => {
const locales = ['th-TH', 'th']
for (const locale of locales) {
const format = detectHourFormat(locale)
expect(format).toBe('hour24')
}
})

test('should return hour12 for India locale', () => {
const locales = ['hi-IN', 'hi']
for (const locale of locales) {
const format = detectHourFormat(locale)
expect(format).toBe('hour12')
}
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, test, expect } from 'bun:test'
import { formatLocalizedDate } from './locale'

describe('formatLocalizedDate', () => {
const testDate = new Date(2023, 11, 25) // December 25, 2023

test('should format date correctly for different Latin locales', () => {
const locales = [
{ locale: 'en-US', expected: 'December 25, 2023' },
{ locale: 'en-GB', expected: '25 December 2023' },
{ locale: 'de-DE', expected: '25. Dezember 2023' },
{ locale: 'ru-RU', expected: '25 декабря 2023 г.' },
]

for (const { locale, expected } of locales) {
const formatted = formatLocalizedDate(testDate, locale)
expect(formatted).toBe(expected)
}
})

test('should format date correctly for non-Latin locales', () => {
const nonLatinLocales = [
{ locale: 'ja-JP', expected: '2023年12月25日' },
{ locale: 'zh-CN', expected: '2023年12月25日' },
{ locale: 'th-TH', expected: '25 ธันวาคม 2566' },
{ locale: 'hi-IN', expected: '25 दिसंबर 2023' },
]

for (const { locale, expected } of nonLatinLocales) {
const formatted = formatLocalizedDate(testDate, locale)
expect(formatted).toBe(expected)
}
})

test('should format date correctly with shorthand locale notations', () => {
const shorthandLocales = [
{ locale: 'en', expected: 'Dec 25, 2023' },
{ locale: 'en-US', expected: 'Dec 25, 2023' },
{ locale: 'en-GB', expected: '25 Dec 2023' },
{ locale: 'de', expected: '25. Dez. 2023' },
{ locale: 'de-DE', expected: '25. Dez. 2023' },
{ locale: 'ja', expected: '2023年12月25日' },
{ locale: 'ja-JP', expected: '2023年12月25日' },
{ locale: 'zh', expected: '2023年12月25日' },
{ locale: 'zh-CN', expected: '2023年12月25日' },
{ locale: 'th', expected: '25 ธ.ค. 2566' },
{ locale: 'th-TH', expected: '25 ธ.ค. 2566' },
{ locale: 'hi', expected: '25 दिस॰ 2023' },
{ locale: 'hi-IN', expected: '25 दिस॰ 2023' },
]

for (const { locale, expected } of shorthandLocales) {
const formatted = formatLocalizedDate(testDate, locale, {
month: 'short',
})
expect(formatted).toBe(expected)
}
})

test('should respect custom options parameter', () => {
const formatted = formatLocalizedDate(testDate, 'en-US', {
year: '2-digit',
month: 'short',
day: '2-digit',
})
expect(formatted).toBe('Dec 25, 23')
})

test('should fallback to en-US for invalid locale', () => {
const formatted = formatLocalizedDate(testDate, 'invalid-locale')
expect(formatted).toBe('December 25, 2023')
})
})
150 changes: 150 additions & 0 deletions edge-apps/edge-apps-library/src/utils/format-time.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, test, expect } from 'bun:test'
import { formatTime } from './locale'

describe('formatTime', () => {
const testDate = new Date('2023-12-25T14:30:45Z')

test('should format time correctly for en-US locale with hour12', () => {
const result = formatTime(testDate, 'en-US', 'UTC', { hour12: true })
expect(result).toHaveProperty('hour')
expect(result).toHaveProperty('minute')
expect(result).toHaveProperty('second')
expect(result).toHaveProperty('dayPeriod')
expect(result).toHaveProperty('formatted')
expect(result.hour).toBe('02')
expect(result.minute).toBe('30')
expect(result.second).toBe('45')
expect(result.dayPeriod).toBe('PM')
expect(result.formatted).toBe('02:30:45 PM')
})

test('should format time correctly for en-US locale with hour24', () => {
const result = formatTime(testDate, 'en-US', 'UTC', { hour12: false })
expect(result.hour).toBe('14')
expect(result.minute).toBe('30')
expect(result.second).toBe('45')
expect(result.dayPeriod).toBeUndefined()
expect(result.formatted).toBe('14:30:45')
})

test('should format time correctly for de-DE locale (24-hour)', () => {
const result = formatTime(testDate, 'de-DE', 'UTC')
expect(result.hour).toBe('14')
expect(result.minute).toBe('30')
expect(result.second).toBe('45')
expect(result.dayPeriod).toBeUndefined()
expect(result.formatted).toBe('14:30:45')
})

test('should format time correctly for ja-JP locale (24-hour)', () => {
const result = formatTime(testDate, 'ja-JP', 'UTC')
expect(result.hour).toBe('14')
expect(result.minute).toBe('30')
expect(result.second).toBe('45')
expect(result.dayPeriod).toBeUndefined()
expect(result.formatted).toBe('14:30:45')
})

test('should format time correctly with different timezones', () => {
const result = formatTime(testDate, 'en-US', 'America/Los_Angeles', {
hour12: true,
})
expect(result).toHaveProperty('hour')
expect(result).toHaveProperty('minute')
expect(result).toHaveProperty('second')
expect(result.minute).toBe('30')
expect(result.second).toBe('45')
// Los Angeles is UTC-8 in December, so 14:30 UTC becomes 06:30 AM
expect(result.hour).toBe('06')
expect(result.dayPeriod).toBe('AM')
expect(result.formatted).toBe('06:30:45 AM')
})

test('should format time correctly with Tokyo timezone', () => {
const result = formatTime(testDate, 'ja-JP', 'Asia/Tokyo')
expect(result.minute).toBe('30')
expect(result.second).toBe('45')
// Tokyo is UTC+9, so 14:30 UTC becomes 23:30
expect(result.hour).toBe('23')
expect(result.formatted).toBe('23:30:45')
})

test('should format time correctly for hi-IN locale (hour12)', () => {
const result = formatTime(testDate, 'hi-IN', 'UTC')
expect(result.hour).toBe('02')
expect(result.minute).toBe('30')
expect(result.second).toBe('45')
expect(result.dayPeriod).toBe('pm')
expect(result.formatted).toMatch(/02:30:45\s+pm/)
})

test('should format time correctly for zh-CN locale (24-hour)', () => {
const result = formatTime(testDate, 'zh-CN', 'UTC')
expect(result.hour).toBe('一四')
expect(result.minute).toBe('三〇')
expect(result.second).toBe('四五')
expect(result.dayPeriod).toBeUndefined()
// Chinese locale uses Chinese numerals: 一四:三〇:四五
expect(result.formatted).toBe('一四:三〇:四五')
})

test('should format time correctly for th-TH locale (24-hour)', () => {
const result = formatTime(testDate, 'th-TH', 'UTC')
expect(result.hour).toBe('๑๔')
expect(result.minute).toBe('๓๐')
expect(result.second).toBe('๔๕')
expect(result.dayPeriod).toBeUndefined()
// Thai locale uses Thai numerals: ๑๔:๓๐:๔๕
expect(result.formatted).toBe('๑๔:๓๐:๔๕')
})

test('should format time correctly for ru-RU locale (24-hour)', () => {
const result = formatTime(testDate, 'ru-RU', 'UTC')
expect(result.hour).toBe('14')
expect(result.minute).toBe('30')
expect(result.second).toBe('45')
expect(result.dayPeriod).toBeUndefined()
expect(result.formatted).toBe('14:30:45')
})

test('should return formatted string property', () => {
const result = formatTime(testDate, 'en-US', 'UTC', { hour12: true })
expect(result.formatted).toBeDefined()
expect(typeof result.formatted).toBe('string')
// Verify formatted contains the time components
expect(result.formatted).toContain('02')
expect(result.formatted).toContain('30')
expect(result.formatted).toContain('45')
expect(result.formatted).toContain('PM')
expect(result.formatted).toMatch(/02:30:45/)
expect(result.formatted).toBe('02:30:45 PM')
})

test('should handle invalid timezone gracefully with fallback', () => {
const originalWarn = console.warn
console.warn = () => {} // Suppress expected warnings
try {
const result = formatTime(testDate, 'en-US', 'Invalid/Timezone')
expect(result).toHaveProperty('hour')
expect(result).toHaveProperty('minute')
expect(result).toHaveProperty('second')
expect(result).toHaveProperty('formatted')
} finally {
console.warn = originalWarn
}
})

test('should handle locales with existing extensions', () => {
// Test with a locale that has existing extensions
// zh-CN-u-ca-chinese has calendar extension
const result = formatTime(testDate, 'zh-CN-u-ca-chinese', 'UTC')
expect(result).toHaveProperty('hour')
expect(result).toHaveProperty('minute')
expect(result).toHaveProperty('second')
expect(result).toHaveProperty('formatted')
// Should use Chinese numerals despite existing extension
expect(result.hour).toBe('一四')
expect(result.minute).toBe('三〇')
expect(result.second).toBe('四五')
})
})
Loading