@@ -109,6 +109,7 @@ LOCAL_HOLIDAY_NAMES: dict[str, dict[str, tuple[str, str]]] = {
" AU " : ( " Boxing Day (observed) " , " 英语 " ) , " GB " : ( " Boxing Day (observed) " , " 英语 " ) ,
} ,
" lunar-new-year " : { " CN " : ( " 春节 " , " 简体中文 " ) , " KR " : ( " 설날 " , " 韩语 " ) , " VN " : ( " Tết Nguyên Đán " , " 越南语 " ) , " SG " : ( " 农历新年 / Chinese New Year " , " 简体中文 / 英语 " ) } ,
" new-year-eve " : { " DK " : ( " Nytårsaften " , " 丹麦语 " ) , " SE " : ( " Nyårsafton " , " 瑞典语 " ) , " PH " : ( " 一年中的最后一天 " , " 英语 / 菲律宾语 " ) } ,
" eid-fitr " : { " AE " : ( " عيد الفطر " , " 阿拉伯语 " ) , " TR " : ( " Ramazan Bayramı " , " 土耳其语 " ) , " ID " : ( " Idul Fitri " , " 印度尼西亚语 " ) } ,
" eid-adha " : { " AE " : ( " عيد الأضحى " , " 阿拉伯语 " ) , " TR " : ( " Kurban Bayramı " , " 土耳其语 " ) , " ID " : ( " Idul Adha " , " 印度尼西亚语 " ) } ,
" assumption-day " : {
@@ -118,6 +119,11 @@ LOCAL_HOLIDAY_NAMES: dict[str, dict[str, tuple[str, str]]] = {
" PT " : ( " Assunção de Nossa Senhora " , " 葡萄牙语 " ) , " ES " : ( " Asunción " , " 西班牙语 " ) ,
} ,
" indonesia-independence " : { " ID " : ( " Hari Kemerdekaan Republik Indonesia " , " 印度尼西亚语 " ) } ,
" new-year " : {
" CN " : ( " 元旦 " , " 简体中文 " ) , " JP " : ( " 元日 " , " 日语 " ) , " RU " : ( " Новогодние каникулы " , " 俄语 " ) ,
} ,
" public-holiday:JP:coming of age day " : { " JP " : ( " 成人の日 " , " 日语 " ) } ,
" public-holiday:JP:greenery day " : { " JP " : ( " みどりの日 " , " 日语 " ) } ,
}
FESTIVAL_TARGET_COUNTRIES = {
@@ -139,6 +145,10 @@ HOLIDAY_TRANSLATIONS = {
" whit monday " : " 圣灵降临节星期一 " , " pentecost monday " : " 圣灵降临节星期一 " ,
" king ' s birthday " : " 国王诞辰 " , " mountain day " : " 山之日 " , " marine day " : " 海之日 " ,
" respect for the aged day " : " 敬老日 " , " culture day " : " 文化日 " , " children ' s day " : " 儿童节 " ,
" new year s day " : " 元旦 " , " new year holiday " : " 新年假期 " ,
" new year s eve " : " 跨年夜 " , " last day of the year " : " 跨年夜 " ,
" coming of age day " : " 成人节 " , " greenery day " : " 绿之日 " ,
" labour thanksgiving day " : " 勤劳感谢之日 " , " labor thanksgiving day " : " 勤劳感谢之日 " ,
}
SUBDIVISION_NAMES = {
@@ -282,6 +292,12 @@ FESTIVAL_PROFILES = {
" design " : [ " 使用新年焕新而非单一宗教表达 " , " 按南北半球季节调整材质 " ] ,
" avoid " : [ " 南半球正值夏季,不要统一推保暖产品 " ] ,
} ,
" new-year-eve " : {
" title " : " 跨年夜 " , " relevance " : " low " , " decision " : " 仅用于运营排班 " ,
" what " : " 公历新年前夜或年末公共休假节点,主要影响跨年营业、物流和客服排班。 " ,
" scenes " : [ " 跨年营业 " , " 物流排班 " ] , " products " : [ ] , " design " : [ ] ,
" avoid " : [ " 不要把跨年夜与元旦或连续新年假期拆成多个重复消费节 " ] ,
} ,
" super-bowl " : {
" title " : " 超级碗观赛消费节点 " , " relevance " : " high " , " decision " : " 值得场景备货 " ,
" what " : " 美国职业橄榄球年度决赛,带动家庭观赛、聚会、酒店和城市活动消费。 " ,
@@ -371,9 +387,11 @@ FESTIVAL_PROFILES = {
GLOBAL_AGGREGATE_KEYS = {
" black-friday " , " cyber-monday " , " winter-holiday " , " christmas " , " boxing-day " ,
" lunar-new-year " , " eid-fitr " , " eid-adha " , " easter " , " assumption-day " , " new-year " , " st-stephens-day " ,
" lunar-new-year " , " eid-fitr " , " eid-adha " , " easter " , " assumption-day " , " new-year " , " new-year-eve " , " st-stephens-day " ,
}
RANGE_AGGREGATE_KEYS = { " new-year " , " eid-fitr " , " eid-adha " , " lunar-new-year " }
FESTIVAL_ENGLISH_TITLES = {
" back-to-school " : " Back to School " ,
" black-friday " : " Black Friday " ,
@@ -385,6 +403,7 @@ FESTIVAL_ENGLISH_TITLES = {
" eid-fitr " : " Eid al-Fitr " ,
" lunar-new-year " : " Lunar New Year " ,
" new-year " : " New Year ' s Day " ,
" new-year-eve " : " New Year ' s Eve " ,
" singles-day " : " Singles ' Day " ,
" st-stephens-day " : " St. Stephen ' s Day " ,
" super-bowl " : " Super Bowl " ,
@@ -393,6 +412,7 @@ FESTIVAL_ENGLISH_TITLES = {
}
GENERIC_HOLIDAY_TITLES = { " 国家纪念与公共假日 " , " 地区公共假日 " }
UNSAFE_CROSS_COUNTRY_TITLES = { " 独立日 " , " 国庆日 " , " 共和国日 " , " 宪法日 " , " 解放日 " , " 胜利日 " , " 纪念日 " }
HOLIDAY_TITLE_ALIASES = { " 五一劳动节 " : " 劳动节 " }
@@ -446,7 +466,9 @@ def festival_key(name: str, source_id: str = "", country_code: str = "") -> str:
return " eid-adha "
if any ( token in lower for token in ( " easter " , " good friday " , " pentecost " , " whit monday " , " 复活节 " , " 耶稣受难 " ) ) :
return " easter "
if any ( token in lower for token in ( " new year ' s day " , " new year holiday " , " 元旦 " , " 新年假期 " ) ) :
if any ( token in lower for token in ( " new year s eve " , " last day of the year " , " 跨年夜 " ) ) :
return " new-year-eve "
if any ( token in lower for token in ( " new year s day " , " new year holiday " , " new year s holiday " , " 元旦 " , " 新年假期 " ) ) :
return " new-year "
if any ( token in lower for token in ( " memorial " , " remembrance " , " all souls " , " 追悼 " , " 纪念 " ) ) :
return " memorial "
@@ -477,7 +499,12 @@ def calendar_event_type(name: str, source_id: str = "") -> str:
def holiday_title ( local_name : str , english_name : str , country_code : str ) - > str :
translated = HOLIDAY_TRANSLATIONS . get ( english_name . lower ( ) )
normalized = _normalized_name ( english_name )
if country_code == " ID " and normalized == " independence day " :
return " 印度尼西亚独立日 "
if country_code == " PH " and normalized == " national heroes day " :
return " 菲律宾国家英雄日 "
translated = HOLIDAY_TRANSLATIONS . get ( english_name . lower ( ) ) or HOLIDAY_TRANSLATIONS . get ( _normalized_name ( english_name ) )
if translated :
return translated
if country_code in ( " CN " , " JP " , " KR " ) and local_name :
@@ -650,17 +677,20 @@ def culture_reference_url(title: str) -> str:
return f " https://zh.wikipedia.org/wiki/ { quote ( title . replace ( ' ' , ' _ ' ) , safe = ' ' ) } "
def culture_reference_cn_url ( title : str ) - > str :
def culture_reference_cn_url ( title : str , festival_key : str = " " ) - > str :
""" 中国大陆访客使用国内网络更易访问的百度百科。 """
return f " https://baike.baidu.com/item/ { quote ( title , safe = ' ' ) } "
aliases = { " new-year " : " 元旦 " }
return f " https://baike.baidu.com/item/ { quote ( aliases . get ( festival_key , title ) , safe = ' ' ) } "
def visual_reference_url ( title : str , country_details : list [ dict [ str , Any ] ] ) - > str :
def visual_reference_url ( title : str , country_details : list [ dict [ str , Any ] ] , title_english : str = " " ) - > str :
""" 图片入口只搜索节日本身,不复用来源页标题。 """
terms = [ title ]
for detail in country_details [ : 2 ] :
terms . extend ( ( str ( detail . get ( " local_name " ) or " " ) , str ( detail . get ( " country " ) or " " ) ) )
query = " " . join ( _dedupe ( terms ) )
detail = next ( ( row for row in country_details if row . get ( " country " ) ) , { } )
query_title = title_english . strip ( ) or str ( detail . get ( " local_name " ) or title ) . strip ( )
if query_title in GENERIC_HOLIDAY_TITLES or not query_title :
query_title = str ( detail . get ( " local_name " ) or title ) . strip ( )
country = str ( detail . get ( " country_english " ) or detail . get ( " country " ) or " " ) . strip ( )
query = " " . join ( _dedupe ( [ query_title , country , " holiday " ] ) ) [ : 120 ]
return f " https://www.bing.com/images/search?q= { quote ( query , safe = ' ' ) } &setlang=zh-hans "
@@ -668,7 +698,11 @@ def calendar_group_identity(item: dict[str, Any]) -> tuple[str, str]:
""" 同日同名的明确节日跨国家聚合;国家纪念类泛称仍按国家拆分。 """
key = str ( item . get ( " festival_key " ) or " public-holiday " )
title = HOLIDAY_TITLE_ALIASES . get ( str ( item . get ( " title " ) or " " ) . strip ( ) , str ( item . get ( " title " ) or " " ) . strip ( ) )
if title and title not in GENERIC_HOLIDAY_TITLES and ( key . startswith ( " public-holiday: " ) or key == " memorial " ) :
if title and title not in GENERIC_HOLIDAY_TITLES and (
key . startswith ( " public-holiday: " )
or key == " memorial "
or ( key . startswith ( " national: " ) and title not in UNSAFE_CROSS_COUNTRY_TITLES )
) :
return f " named-holiday: { _normalized_name ( title ) } " , " "
country_partition = " " if key in GLOBAL_AGGREGATE_KEYS else str ( item . get ( " country_code " ) or item . get ( " market_area " ) or item . get ( " region " ) or " " )
return key , country_partition
@@ -677,6 +711,8 @@ def calendar_group_identity(item: dict[str, Any]) -> tuple[str, str]:
def calendar_group_date ( item : dict [ str , Any ] ) - > str :
""" 圣诞季共用一张决策卡,其他节日仍按实际日期聚合。 """
start_date = str ( item [ " start_date " ] )
if item . get ( " festival_key " ) in RANGE_AGGREGATE_KEYS :
return f " @ { start_date [ : 4 ] } "
if item . get ( " festival_key " ) != " christmas " :
return start_date
value = date . fromisoformat ( start_date )
@@ -704,8 +740,15 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
grouped . setdefault ( ( calendar_group_date ( item ) , group_key , country_partition ) , [ ] ) . append ( item )
insights : list [ dict [ str , Any ] ] = [ ]
for ( start_date , group_key , _ ) , rows in grouped . items ( ) :
rows = sorted ( rows , key = lambda row : ( str ( row . get ( " start_date " ) ) != start_date , str ( row . get ( " start_date " ) ) ) )
for ( group_date , group_key , _ ) , rows in grouped . items ( ) :
range_group = group_date . startswith ( " @ " )
start_date = min ( str ( row . get ( " start_date " ) or group_date ) for row in rows ) if range_group else group_date
rows = sorted ( rows , key = lambda row : (
str ( row . get ( " start_date " ) ) != start_date ,
0 if str ( row . get ( " country_code " ) or " " ) == " CN " else 1 ,
str ( row . get ( " country_code " ) or " " ) ,
str ( row . get ( " start_date " ) or " " ) ,
) )
row_keys = _dedupe ( [ str ( row . get ( " festival_key " ) or " public-holiday " ) for row in rows ] )
key = row_keys [ 0 ] if len ( row_keys ) == 1 else f " public-holiday: { group_key . removeprefix ( ' named-holiday: ' ) } "
profile = profile_for_market ( key , str ( rows [ 0 ] . get ( " country_code " ) or " " ) )
@@ -715,7 +758,7 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
platforms = _dedupe ( [ value for row in rows for value in row . get ( " platforms " ) or [ ] ] )
titles = [ str ( row . get ( " title " ) or " " ) for row in rows if row . get ( " title " ) ]
title = str ( profile . get ( " title " ) or Counter ( titles ) . most_common ( 1 ) [ 0 ] [ 0 ] )
if key . startswith ( " public-holiday: " ) or key in ( " national " , " memorial " ) :
if key . startswith ( ( " public-holiday: " , " national: " ) ) or key in ( " national " , " memorial " ) :
title = Counter ( titles ) . most_common ( 1 ) [ 0 ] [ 0 ]
english_titles = [ str ( row . get ( " title_english " ) or " " ) . strip ( ) for row in rows if row . get ( " title_english " ) ]
title_english = FESTIVAL_ENGLISH_TITLES . get ( key , " " )
@@ -822,6 +865,9 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
" label " : christmas_date_label ( value , start_date ) if key == " christmas " else " 地区日期 " ,
" countries " : variant_countries ,
} )
end_date = start_date if key == " christmas " else max ( str ( row . get ( " end_date " ) or start_date ) for row in rows )
if key == " new-year " and " RU " in country_codes :
end_date = max ( end_date , f " { start_date [ : 4 ] } -01-08 " )
insight = {
" id " : f " calendar-insight- { digest } " ,
" section " : " calendar " ,
@@ -842,7 +888,7 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
" change_status " : " 官方日期已核验 " ,
" confidence " : " 高 " if official_sources else " 中高 " ,
" start_date " : start_date ,
" end_date " : start_date if key == " christmas " else max ( str ( row . get ( " end_date " ) or start_date ) for row in rows ) ,
" end_date " : end_date ,
" related_dates " : related_dates ,
" date_variants " : date_variants ,
" date_variation_note " : (
@@ -876,9 +922,9 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
" days_until " : ( date . fromisoformat ( start_date ) - now ) . days ,
" priority_score " : max ( int ( row . get ( " priority_score " ) or 0 ) for row in rows ) + min ( 12 , len ( countries ) ) ,
" analysis_basis " : " 事实层:官方日期与适用国家;分析层:家纺经营场景推断 " ,
" visual_reference_url " : visual_reference_url ( title , country_details ) ,
" visual_reference_url " : visual_reference_url ( title , country_details , title_english ) ,
" culture_reference_url " : culture_reference_url ( title ) ,
" culture_reference_cn_url " : culture_reference_cn_url ( title ) ,
" culture_reference_cn_url " : culture_reference_cn_url ( title , key ) ,
}
translation = {
key : first [ key ]
@@ -889,6 +935,17 @@ def aggregate_calendar_insights(items: list[dict[str, Any]], now: date, poster_f
)
if key in first
}
if " CN " in country_codes :
translation . update ( {
" source_language " : " zh-CN " ,
" needs_translation " : False ,
" google_translation_supported " : False ,
" translation_url " : " " ,
" translation_provider " : " " ,
" translation_fallback_url " : " " ,
" translation_fallback_provider " : " " ,
" translation_notice " : " 该节日包含中国市场,中文访客直接查看中文节日资料。 " ,
} )
insight . update ( translation )
insight [ " poster " ] = poster_for ( insight [ " id " ] , " calendar " , insight [ " culture_key " ] )
insights . append ( insight )