locale-procs.tcl

Does not contain a contract.

Location:
/packages/acs-lang/tcl/locale-procs.tcl

Related Files

[ hide source ] | [ make this the default ]

File Contents

ad_library {

    Localization procedures for OpenACS
    <p>
    This is free software distributed under the terms of the GNU Public
    License.  Full text of the license is available from the GNU Project:
    http://www.fsf.org/copyleft/gpl.html

    @creation-date 28 September 2000
    @author Henry Minsky (hqm@mit.edu)
    @author Lars Pind (lars@pinds.com)
    @cvs-id $Id: locale-procs.tcl,v 1.53 2024/09/11 06:15:48 gustafn Exp $
}

namespace eval lang::system {}
namespace eval lang::user {}
namespace eval lang::conn {}



#####
#
# lang::system
#
#####

ad_proc -public lang::system::use_package_level_locales_p {} {
    Returns whether we're using package level locales.
} {
    return [parameter::get -parameter UsePackageLevelLocalesP -package_id [apm_package_id_from_key "acs-lang"] -default 0]
}

d_proc -public lang::system::site_wide_locale {
} {
    Get the site-wide system locale setting.
} {
    set parameter_locale [parameter::get \
                -package_id [apm_package_id_from_key "acs-lang"] \
                -parameter "SiteWideLocale" \
                -default "en_US"]

    # Check validity of parameter setting
    set valid_locales [lang::system::get_locales]
    if {$parameter_locale ni $valid_locales} {
        ns_log Error "The parameter setting acs-lang.SiteWideLocale=\"$parameter_locale\" is invalid. Valid locales are: \"$valid_locales\". Defaulting to en_US locale"
        return en_US
    }

    return $parameter_locale
}

d_proc -private lang::system::package_level_locale_not_cached {
    package_id
} {
    return [db_string get_package_locale {} -default {}]
}

d_proc -public lang::system::package_level_locale {
    package_id
} {
    @return empty string if not use_package_level_locales_p, or the package locale from apm_packages table.
} {
    if { ![use_package_level_locales_p] } {
        return {}
    }

    return [util_memoize [list lang::system::package_level_locale_not_cached $package_id]]
}

d_proc -public lang::system::locale {
    {-package_id ""}
    {-site_wide:boolean}
} {
    Get system locale setting for a given package instance.

    @param package_id The package for which you want to get the locale setting.
    @param site_wide Set this if you want to get the site-wide locale setting.
} {
    if { $site_wide_p } {
        return [site_wide_locale]
    }

    if { $package_id eq "" && [ns_conn isconnected] } {
        set package_id [ad_conn package_id]
    }

    # Get locale for package

    set locale [package_level_locale $package_id]

    # If there's no package setting, use the site-wide setting
    if { $locale eq "" } {
        set locale [site_wide_locale]
    }
    return $locale
}

d_proc -public lang::system::set_locale {
    {-package_id ""}
    locale
} {
    Set system locale setting for a given package instance, or the
    site-wide system locale.

    @param package_id The package for which you want to set the locale setting, if you want to set system setting for one package only. Leave blank for site-wide setting.
    @param locale The new locale that you want to use as your system locale.
} {
    if { $package_id eq "" } {

        parameter::set_value \
            -package_id [apm_package_id_from_key "acs-lang"] \
            -parameter SiteWideLocale \
            -value $locale

    } else {
        # Update the setting
        db_dml update_system_locale {}

        # Flush the cache
        util_memoize_flush [list lang::system::package_level_locale_not_cached $package_id]

        # TODO: We will need to have site-map inheritance for this, so packages under a subsite/dotlrn inherit the subsite's/dotlrn's setting
    }
}

d_proc -public lang::system::language {
    {-package_id ""}
    {-site_wide:boolean}
    {-iso6392:boolean}
} {
    Get system language setting for a given package instance.

    @param package_id The package for which you want to get the language setting.
    @param site_wide Set this if you want to get the site-wide language setting.
    @param iso6392   Set this if you want to force iso-639-2 code (3 digits)

    @return 3 chars language code if iso6392 is set, left part of locale otherwise

} {
    set locale [locale -package_id $package_id -site_wide=$site_wide_p]
    set sys_lang [lindex [split $locale "_"] 0]

    if { $iso6392_p } {
        return [lang::util::iso6392_from_language -language $sys_lang]
    } else {
        return $sys_lang
    }
}

ad_proc -public lang::system::timezone {} {
    Ask OpenACS what it thinks our timezone is.

    @return  a timezone name from acs-reference package (e.g., Asia/Tokyo, America/New_York)
} {
    set package_id [apm_package_id_from_key "acs-lang"]
    return [parameter::get -package_id $package_id -parameter SystemTimezone -default "Etc/UTC"]
}

d_proc -public lang::system::set_timezone {
    timezone
}  {
    Tell OpenACS what timezone we think it's running in.

    @param timezone name from acs-reference package (e.g., Asia/Tokyo, America/New_York)
} {
    set package_id [apm_package_id_from_key "acs-lang"]
    parameter::set_value -package_id $package_id -parameter SystemTimezone -value $timezone
}

ad_proc -public lang::system::timezone_utc_offset { } {
    @return number of hours to subtract from local (database) time to get UTC
} {
    set system_timezone [timezone]
    return [db_string system_utc_offset {}]
}

d_proc -public lang::system::get_locales {
    {-all:boolean}
} {

    Return all locales defined in the system. Per default only the
    enabled locales are returned. When the optional flag "-all" is
    specified, all defined locales are returned.
    
    This value is cached per thread and needs currently a server
    restart, when the system locales are changed.

    @author Peter Marklund
} {
    return [acs::per_thread_cache eval -key acs-lang.system_get_locales-$all_p {
        if {$all_p} {
            db_list select_defined_system_locales { select locale from ad_locales }
        } else {
            db_list select_enabled_system_locales {
                select locale
                from   ad_locales
                where  enabled_p = 't'
            }
        }
    }]
}

ad_proc -public lang::system::get_locale_options {} {
    Return all enabled locales in the system in a format suitable for the options argument of a form.

    @author Lars Pind
} {
    return [util_memoize lang::system::get_locale_options_not_cached]
}

d_proc -public lang::system::locale_set_enabled {
    {-locale:required}
    {-enabled_p:required}
} {
    Enables or disables a locale.

    @param enabled_p Should be t or f

    @author Peter Marklund
} {
    db_dml set_enabled_p { update ad_locales set enabled_p = :enabled_p where locale = :locale }

    # Flush caches
    acs::per_thread_cache flush -pattern acs-lang.system_get_locales-*
    util_memoize_flush_regexp {^lang::util::default_locale_from_lang_not_cached}
    util_memoize_flush_regexp {^lang::system::get_locales}
    util_memoize_flush_regexp {^lang::system::get_locale_options}
}


ad_proc -private lang::system::get_locale_options_not_cached {} {
    Return all enabled locales in the system in a format suitable for the options argument of a form.

    @author Lars Pind
} {
    return [db_list_of_lists select_locales {}]
}


#####
#
# lang::user
#
#####

d_proc -private lang::user::package_level_locale_not_cached {
    user_id
    package_id
} {
    Get the user's preferred package level locale for a package
    given by its package id. Will return the empty string if the
    user has not preference for the package.
} {
    return [db_string get_user_locale {} -default ""]
}

d_proc -public lang::user::package_level_locale {
    {-user_id ""}
    package_id
} {
    Get the user's preferred package level locale for a package
    given by its package id.
} {
    # default to current user
    if { $user_id eq "" } {
        set user_id [ad_conn untrusted_user_id]
    }

    # If package-level locales are turned off, or the user isn't logged in, return the empty string
    if { ![lang::system::use_package_level_locales_p] || $user_id == 0 } {
        return {}
    }

    # Cache for the lifetime of sessions (7 days)
    return [util_memoize [list lang::user::package_level_locale_not_cached $user_id $package_id] [sec_session_timeout]]
}

d_proc -public lang::user::site_wide_locale {
    {-user_id ""}
} {
    Get the user's preferred site-wide locale.
} {
    # default to current user
    if { $user_id eq "" } {
        set user_id [ad_conn untrusted_user_id]
    }

    # For all the users with a user_id of 0 don't cache.
    if { $user_id == 0} {
        return [lang::user::site_wide_locale_not_cached $user_id]
    }

    # Cache for the lifetime of sessions (7 days)
    return [util_memoize [list lang::user::site_wide_locale_not_cached $user_id] [sec_session_timeout]]
}

d_proc -private lang::user::site_wide_locale_not_cached {
    user_id
} {
    Get the user's preferred site-wide locale.
} {
    set system_locale [lang::system::site_wide_locale]

    if { $user_id == 0 } {
        set cookie_name [security::cookie_name locale]
        set locale [ad_get_cookie $cookie_name]
        if {$locale ne ""} {
            #
            # Check, if someone hacked the cookie
            #
            if {$locale ni [lang::system::get_locales]} {
                ns_log warning "ignoring invalid ad_locale cookie '$locale'"
                set locale ""
                #
                # The cookie was invalid, so get rid of it.
                #
                ad_unset_cookie $cookie_name
            }
        }
    } else {
        set locale [db_string get_user_site_wide_locale {} -default ""]
    }

    #
    # When no locale cookie is set, or the locale is invalid or empty,
    # fall back to system locale.
    #
    if { $locale eq "" } {
        set locale $system_locale
    }

    return $locale
}

d_proc -public lang::user::locale {
    {-package_id ""}
    {-site_wide:boolean}
    {-user_id ""}
} {
    Get user locale preference for a given package instance.

    @param package_id The package for which you want to get the locale preference.
    @param site_wide Set this if you want to get the site-wide locale preference.
    @param user_id Set this to the user you want to get the locale of, defaults to current user.
} {
    # default to current user
    if { $user_id eq "" } {
        set user_id [ad_conn untrusted_user_id]
    }

    # default to current connection package
    if { $package_id eq "" } {
        set package_id [ad_conn package_id]
    }

    if {$site_wide_p} {
        set locale [site_wide_locale -user_id $user_id]
    } else {
        #
        # Try package level locale first unless site_wide_p was
        # specified.
        #
        set locale [package_level_locale -user_id $user_id $package_id]
        #
        # If there's no package setting, then use the site-wide
        # setting.
        #
        if { $locale eq "" } {
            set locale [site_wide_locale -user_id $user_id]
        }
    }

    return $locale
}

d_proc -public lang::user::set_locale {
    {-package_id ""}
    {-user_id ""}
    locale
} {
    Set user locale setting for a given package instance.

    @param package_id The package for which you want to set the locale setting, if you want to set it for a specific package, as opposed to a site-wide setting.
    @param locale The new locale that you want to use as your system locale.
} {
    if { $user_id eq "" } {
        set user_id [ad_conn user_id]
    }

    if { $user_id == 0 } {
        # Not logged in, use a cookie-based client locale
        set cookie_name [security::cookie_name locale]
        ad_set_cookie -replace t -max_age inf -samesite strict $cookie_name $locale

        # Flush the site-wide user preference cache
        util_memoize_flush [list lang::user::site_wide_locale_not_cached $user_id]
        return
    }

    if { $package_id eq "" } {
        # Set site-wide locale in user_preferences table
        db_dml set_user_site_wide_locale {}

        # Flush the site-wide user preference cache
        util_memoize_flush [list lang::user::site_wide_locale_not_cached $user_id]
        return
    }

    # The rest is for package level locale settings only
    # Even if package level locales are disabled, we'll still do this

    set user_locale_exists_p [db_string user_locale_exists_p {}]
    if { $user_locale_exists_p } {
        if { $locale ne "" } {
            db_dml update_user_locale {}
        } else {
            db_dml delete_user_locale {}
        }
    } else {
        if { $locale ne "" } {
            db_dml insert_user_locale {}
        }
    }

    # Flush the user locale preference cache
    util_memoize_flush [list lang::user::package_level_locale_not_cached $user_id $package_id]
}

d_proc -public lang::user::language {
    {-package_id ""}
    {-user_id ""}
    {-site_wide:boolean}
    {-iso6392:boolean}
} {
    Get user language preference for a given package instance.
    This preliminary implementation only has one site-wide setting, though.

    @param package_id The package for which you want to get the language setting.
    @param user_id The user we wish to get the language for, defaults to connection user.
    @param site_wide Set this if you want to get the site-wide language setting.
    @param iso6392   Set this if you want to force iso-639-2 code (3 digits)

    @return 3 chars language code if iso6392 is set, left part of locale otherwise

} {
    set locale [locale -package_id $package_id -user_id $user_id -site_wide=$site_wide_p]
    set user_lang [lindex [split $locale "_"] 0]

    if { $iso6392_p } {
        return [lang::util::iso6392_from_language -language $user_lang]
    } else {
        return $user_lang
    }
}


ad_proc -private lang::user::timezone_no_cache {user_id} {
    return [db_string select_user_timezone {} -default ""]
}

ad_proc -public lang::user::timezone {} {
    Get the user's timezone. Returns the empty string if the user
    has no timezone set.

    @return  a timezone name from acs-reference package (e.g., Asia/Tokyo, America/New_York)
} {
    set user_id [ad_conn user_id]
    if { $user_id == 0 } {
        return ""
    }

    return [util_memoize [list lang::user::timezone_no_cache $user_id]]
}

d_proc -public lang::user::set_timezone {
    timezone
}  {
    Set the user's timezone setting.

    @param timezone name from acs-reference package (e.g., Asia/Tokyo, America/New_York)
} {
    set user_id [ad_conn user_id]

    if { $user_id == 0 } {
        error "User not logged in"
    } else {
        db_dml set_user_timezone {}
        util_memoize_flush [list lang::user::timezone_no_cache $user_id]
    }
}





#####
#
# lang::conn
#
#####

d_proc -private lang::conn::locale_not_cached {
    {-package_id ""}
    {-site_wide:boolean}
    {-user_id ""}
} {
    Get the locale for this request, perhaps for a given package instance.

    This is the not-cached version.

    @param package_id The package for which you want to get the locale.
    @param site_wide Set this if you want to get the site-wide locale.
} {
    if { $site_wide_p } {
        set locale [lang::user::site_wide_locale]

        if { $locale eq "" } {
            set locale [lang::system::site_wide_locale]
        }

        #
        # Fallback to en_US when no locale is found or is not one of
        # those we support.
        #
        if { $locale eq "" || $locale ni [lang::system::get_locales]} {
            set locale en_US
        }

        return $locale
    }

    # default value for package_id

    if { $package_id eq "" } {
        set package_id [ad_conn package_id]
    }

    # use user's package level locale

    set locale [lang::user::package_level_locale -user_id $user_id $package_id]

    # if that does not exist use system's package level locale

    if { $locale eq "" } {
        set locale [lang::system::package_level_locale $package_id]
    }

    # if that does not exist use user's site-wide locale

    if { $locale eq "" } {
        set locale [lang::user::site_wide_locale -user_id $user_id]
    }

    # Use the accept-language browser heading

    if { $locale eq "" && [ns_conn isconnected]} {
        set locale [lang::conn::browser_locale]
    }

    # if that does not exist use system's site-wide locale

    if { $locale eq "" } {
        set locale [lang::system::site_wide_locale]
    }

    # if that does not exist, or is not supported, then we are back to
    # just another language let's pick uhmm... en_US

    if { $locale eq "" || $locale ni [lang::system::get_locales]} {
        set locale en_US
    }

    return $locale
}

d_proc -public lang::conn::locale {
    {-package_id ""}
    {-site_wide:boolean}
    {-user_id ""}
} {
    Get the locale for this request, perhaps for a given package instance.

    @param package_id The package for which you want to get the locale.
    @param site_wide Set this if you want to get the site-wide locale.
} {
    # Notice that caching for longer than the single request would be
    # more complex, e.g. defaults coming from ad_conn in the various
    # procs and flushing.
    return [acs::per_request_cache eval \
                -key acs-lang.lang.conn.locale($package_id,$site_wide_p,$user_id) {
                    lang::conn::locale_not_cached \
                        -package_id $package_id \
                        -site_wide=$site_wide_p \
                        -user_id $user_id
                }]
}

ad_proc -private lang::conn::browser_locale {} {
    Get the users preferred locale from the accept-language
    HTTP header.

    @return A locale or an empty string if no locale can be found that
            is supported by the system

    @author Lars Pind
    @author Peter Marklund
} {
    set conn_locales [lang::conn::get_accept_language_header]

    set system_locales [lang::system::get_locales]

    foreach locale $conn_locales {
        regexp {^([^_]+)(?:_([^_]+))?$} $locale locale language region

        if { [info exists region] && $region ne "" } {
            # We have both language and region, e.g. en_US
            if {$locale in $system_locales} {
                # The locale was found in the system, a perfect match
                set perfect_match $locale
                break
            } else {
                # We don't have the full locale in the system but check if
                # we have a different locale with matching language,
                # i.e. a tentative match
                if { ![info exists tentative_match] } {
                    set default_locale [lang::util::default_locale_from_lang $language]
                    if { $default_locale ne "" } {
                        set tentative_match $default_locale
                    }
                } else {
                    # We already have a tentative match with higher priority so
                    # continue searching for a perfect match
                    continue
                }
            }
        } else {
            # We have just a language, e.g. en
            set default_locale [lang::util::default_locale_from_lang $locale]
            if { $default_locale ne "" } {
                set perfect_match $default_locale
                break
            }
        }
    }

    if { [info exists perfect_match] && $perfect_match ne "" } {
        return $perfect_match
    } elseif { [info exists tentative_match] && $tentative_match ne "" } {
        return $tentative_match
    } else {
        # We didn't find a match
        return ""
    }
}

ad_proc -private lang::conn::valid_locale_p {locale} {
    Check, of the provided locale is syntactically correct
} {
    return [regexp {^[a-zA-Z]+(_[a-zA-Z0-9]+)?$} $locale]
}

ad_proc -private lang::conn::get_accept_language_header {} {
    Obtain a list of locals from the request headers.
    @return a list of locales in the syntax used by OpenACS (ISO codes)
} {
    set acclang [ns_set iget [ns_conn headers] "Accept-Language"]

    # Split by comma, and get rid of any ;q=0.5 parts
    # acclang is something like 'da,en-us;q=0.8,es-ni;q=0.5,de;q=0.3'
    set acclangv [list]
    foreach elm [split $acclang ","] {
        # Get rid of trailing ;q=0.5 part and trim spaces
        set elm [string trimleft [lindex [split $elm ";"] 0] " "]
        # Ignore the default catchall setting "*"
        if {$elm eq "*"} {
            continue
        }
        # elm is now either like 'da' or 'en-us'
        # make it into something like 'da' or 'en_US'
        set elmv [split $elm "-"]
        set elm [lindex $elmv 0]
        if { [llength $elmv] > 1 } {
            append elm "_[string toupper [lindex $elmv 1]]"
        }
        if {[lang::conn::valid_locale_p $elm]} {
            lappend acclangv $elm
        } else {
            # It is usually bots or other kinds of not-canonical web
            # browsers which set this wrong. We tolerate it by
            # assuming our default language.
            ns_log warning "Invalid locale '$elm' in provided Accept-Language header field. Defaulting to system locale."
            return [lang::system::locale]
        }
    }

    return $acclangv
}

d_proc -public lang::conn::language {
    {-package_id ""}
    {-user_id ""}
    {-site_wide:boolean}
    {-iso6392:boolean}
    {-locale ""}
} {
    Get the language for this request, perhaps for a given package instance.

    @param package_id The package for which you want to get the language
           (used only when, no locale is provided).
    @param user_id The user_id for whom you want to get the language
           (used only when, no locale is provided).
    @param site_wide Set this if you want to get the site-wide language
               (used only when, no locale is provided).
    @param iso6392   Set this if you want to force the iso-639-2 code
    @param locale    obtain language from provided locale

    @return 3 chars language code if iso6392 is set, left part of locale otherwise
} {
    if {$locale eq ""} {
        set locale [locale -package_id $package_id -user_id $user_id -site_wide=$site_wide_p]
    }
    set conn_lang [lindex [split $locale "_"] 0]

    if { $iso6392_p } {
        return [lang::util::iso6392_from_language -language $conn_lang]
    } else {
        return $conn_lang
    }
}

d_proc -public lang::conn::charset {
} {
    Returns the MIME charset name corresponding to the current connection's locale.

    @author        Lars Pind (lars@pinds.com)

    @return        IANA MIME character set name
} {
    return [lang::util::charset_for_locale [lang::conn::locale]]
}

ad_proc -public lang::conn::timezone {} {
    Get this connection's timezone. This is the user timezone, if
    set, otherwise the system timezone.

    @return  a timezone name from acs-reference package (e.g., Asia/Tokyo, America/New_York)
} {
    set timezone {}
    if { [ad_conn isconnected] } {
        set timezone [lang::user::timezone]
    }

    if { $timezone eq "" } {
        # No user timezone, return the system timezone
        set timezone [lang::system::timezone]
    }
    return $timezone
}

# Local variables:
#    mode: tcl
#    tcl-indent-level: 4
#    indent-tabs-mode: nil
# End: