/* Copyright 2006 Joachim Zobel . * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * mod_i18n implements Zopes i18n namespace in an output filter. It thereby * allows gettext based internationalization of arbitray html content * delivered by apache. * * The i18n attribute that are currently implemented are: * translate, name, domain, target * attribute will soon follow. * * Requirements * mod_xml2 is required for parsing the html. It has to be loaded * before mod_xml2. * The prefork mpm has to be used, since GNU gettext is not threadsafe. * * It is compiled and installed as expected with * /usr/local/apache2/bin/apxs -i -c -I /usr/include/libxml2 mod_i18n.c \ * /usr/local/lib/libxml2.la /usr/lib/libapreq2.la * * Configuration * * The name of the filter is i18n. The sax filter implemented by mod_xml2 * has to be run before. * * Configuration Directives * * I18NLocaleDir * Context: server config, virtual host, directory, .htaccess * The directory that holds the locale directories used by gettext. * * I18NLangParam * Context: server config, virtual host, directory, .htaccess * The GET parameter that is used to pass the language code to the i18n * filter. * * I18NLangParamForce * Context: server config, virtual host, directory, .htaccess * The language set by a request parameter takes precedence over the * language set by target attributes. * */ /* * Choosing a language * * There are 2 ways to choose a language. Both take a language * tag as defined by RFC 2616 (thats what is send with * Accept-Language). * 1. You can configure a GET parameter name to use to * select a language with I18NLangParam. * 2. By an i18n target attribute. * You can interface with content negatiation by including * negotiated target attributes with SSI. * By default target tags have priority over the language * parameter. This can be overriden with I18NLangParamForce. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // #include // #include module AP_MODULE_DECLARE_DATA i18n_module; #include "frag_buffer.h" #include "buckets_sax.h" #include "sax_util.h" #include "tree_transform.h" // Parameters for the transform filter typedef struct { apr_pool_t *pool; // request to use for logging request_rec *r_log; // The directury where the locale is const char *locale_dir; // locale for gettext const char *locale; // The current domain const xml_char_t *domain; } param_transform; // module configuration typedef struct { // The locale directory for gettext. const char *locale_dir; // The name of the request parameter to set the language. const char *lang_param; // The lang. param. overrides int lang_param_force; } i18n_cfg; // The filter context typedef struct { // The current translate bucket, // 0 if there is none. se_id_t translate; // The domains stack apr_array_header_t *domains; // domains are unified unq_set_t *unq_domain; // The targets stack apr_array_header_t *targets; // domains are unified unq_set_t *unq_target; // The tree transformation filter. ap_filter_t *f_tr; // The transformation filters parameters param_transform *param; // The outbound brigade apr_bucket_brigade *bb_out; // A pool that si cleaned between ap_pass_brigade // calls. // apr_pool_t *p_tmp; // The sax context used to create // sax buckets from transformed trees. sax_ctx *sax; // Buckets that are needed // frequently to turn document // fragments into complete documents. apr_bucket *b_xml_decl; } i18n_ctx; // start tag related stack entries typedef struct { // The target start tag se_id_t se_id; // The target const xml_char_t *tag_value; } i18n_tag_entry; static const xml_char_t NS_I18N[] = "http://xml.zope.org/namespaces/i18n"; /***************************************************************************** * Tree Transformation Functions *****************************************************************************/ static void i18n_translate_node(param_transform * param, xmlNodePtr nd); static void i18n_expand_text_to_nodes(param_transform * param, xmlNodePtr nd, apr_hash_t * tr_chld_nd, xmlNodePtr bef, const xml_char_t * trans); static const xml_char_t *i18n_extract_text(param_transform * param, xmlNodePtr nd, apr_hash_t * tr_chld_nd); static xml_char_t *i18n_normalize_ws(xml_char_t * dest, const xml_char_t *); static const xml_char_t *i18n_gettext(param_transform * param, const char *msgid); /* * function: i18n_transform * description: The funtion that is passed to tree_transform.c * parameters: ?? * return: - */ static apr_status_t i18n_transform(void *param, xmlDocPtr doc) { param_transform *ptr = param; sax_check_pool(ptr->pool); sax_check_pool(ptr->r_log->pool); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, ptr->r_log, "i18n_transform called for domain %s.", ptr->domain); xmlNodePtr nd = xmlDocGetRootElement(doc); i18n_translate_node(ptr, nd); sax_check_pool(ptr->r_log->pool); sax_check_pool(ptr->pool); } /** * function: i18n_translate_node * description: Translates the given node and all its children * parameters: nd - a node with an translate or name * attribute * return: - */ static void i18n_translate_node(param_transform * param, xmlNodePtr nd) { apr_pool_t *pool = param->pool; sax_check_pool(param->r_log->pool); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "i18n_translate_node called."); const xml_char_t *trans = xmlGetNsProp(nd, "translate", NS_I18N); const xml_char_t *name = xmlGetNsProp(nd, "name", NS_I18N); if (trans || name) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Trying to translate %s.", trans); apr_hash_t *tr_chld_nd = apr_hash_make(pool); // extract text also translates the child nodes const xml_char_t *xt = i18n_extract_text(param, nd, tr_chld_nd); if (trans && !trans[0]) { // The extracted text is only used if // there was an empty attribute. trans = xt; } if (trans) { trans = i18n_gettext(param, trans); xmlRemoveProp(xmlHasNsProp(nd, "translate", NS_I18N)); } // We add a comment node to prevent merging of adjacent text nodes. xmlNodePtr nd_start_old = xmlAddPrevSibling(nd->children, xmlNewComment("seperator")); i18n_expand_text_to_nodes(param, nd, tr_chld_nd, nd_start_old, trans); // remove the old stuff while (nd_start_old->next) { xmlNodePtr cur = nd_start_old->next; xml_char_t *cont = xmlNodeGetContent(cur); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Unlinking node '%s'.", cont); xmlUnlinkNode(cur); xmlFreeNode(cur); } xml_char_t *cont = xmlNodeGetContent(nd_start_old); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Unlinking node '%s'.", cont); xmlFree(cont); xmlUnlinkNode(nd_start_old); xmlFreeNode(nd_start_old); } else { // Try children xmlNodePtr chld; for (chld = nd->children; chld; chld = chld->next) { i18n_translate_node(param, chld); } } sax_check_pool(param->r_log->pool); } /* * function: i18n_expand_text_to_nodes * description: Expands the given text to child nodes of oNd, * replacing all $ variables with the corresponding * nodes from oTrCh. * parameters: oNd - a node with an translate or name * attribute * oTrCh - a name -> node map * oBef - the insertBefore marker * sTrans - The text to expand * return: - */ static void i18n_expand_text_to_nodes(param_transform * param, xmlNodePtr nd, apr_hash_t * tr_chld_nd, xmlNodePtr bef, const xml_char_t * trans) { apr_pool_t *pool = param->pool; sax_check_pool(param->r_log->pool); static ap_regex_t rx = { NULL, 0, 0 }; if (!rx.re_pcre) { ap_regcomp(&rx, "\\${[^}]+}|\\$\\w+", 0); } if (!trans) { return; } const xml_char_t *run = trans; ap_regmatch_t match = { 0, 0 }; int len = 0; while (!ap_regexec(&rx, run, 1, &match, 0)) { const char *start = run; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Creating text node for %d of '%s'.", match.rm_so, run); xmlAddPrevSibling(bef, xmlNewTextLen(run, match.rm_so)); run += match.rm_so + 1; len = match.rm_eo - match.rm_so - 1; if (run[0] == '{') { run++; len -= 2; } ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Looking up node named by %d of >%.10s from hash.", len, run); xmlNodePtr nnd = apr_hash_get(tr_chld_nd, run, len); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Adding node %x named %s from hash.", nnd, xmlGetNsProp(nnd, "name", NS_I18N)); xmlAddPrevSibling(bef, nnd); xmlRemoveProp(xmlHasNsProp(nnd, "name", NS_I18N)); run = start + match.rm_eo; } // There is one more text ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Creating text node for '%s'.", run); xmlAddPrevSibling(bef, xmlNewText(run)); sax_check_pool(param->r_log->pool); } /***************************************************************************** * Content negotiation *****************************************************************************/ /* * UNUSED * * Retrieve a list of available languages from the gettext locale directory. * @param pool - for memory allocation * @param dirlist - the list to return - an arra of char * * @param dirname - the gettext locale dir * @return APR_SUCCESS on success. */ static apr_status_t i18n_list_languages(apr_pool_t * pool, apr_array_header_t * dirlist, const char *dirname) { apr_dir_t *dirp; apr_finfo_t dirent; // reset dirlist dirlist->nelts = 0; if (apr_dir_open(&dirp, dirname, pool) != APR_SUCCESS) { ap_log_perror(APLOG_MARK, APLOG_ERR, 0, pool, "locale dir %s could not be opened.", dirname); return HTTP_FORBIDDEN; } while (apr_dir_read(&dirent, APR_FINFO_DIRENT, dirp) == APR_SUCCESS) { // ignore all non-dirs if (!(dirent.valid & APR_FINFO_TYPE) || (dirent.filetype != APR_DIR)) { continue; } char **dirnamep = apr_array_push(dirlist); *dirnamep = apr_pstrdup(pool, dirent.name); // Relpacing _ with - is all that is needed // to create language tags from gettext // locale directories. char *p; for (p = *dirnamep; *p; p++) { if (*p == '_') { *p = '-'; } } } apr_dir_close(dirp); return APR_SUCCESS; } /* * function: i18n_extract_text * description: Extract text for translation * parameters: nd - a node with a translate or name * attribute * tr_chld_nd - Translated Children returns the * translated child nodes by name * return: The extracted text. nodes with name attr. are replaced * with ${name} */ static const xml_char_t *i18n_extract_text(param_transform * param, xmlNodePtr nd, apr_hash_t * tr_chld_nd) { apr_pool_t *pool = param->pool; sax_check_pool(param->r_log->pool); frag_buffer_t *rtn = frag_create(pool); // We iterate over the child nodes xmlNodePtr chld; for (chld = nd->children; chld; chld = chld->next) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Extracting node of type %d.", chld->type); switch (chld->type) { case XML_TEXT_NODE: ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Extracting text node %s.", chld->content); frag_write(rtn, chld->content, strlen(chld->content)); break; case XML_ELEMENT_NODE: { const xml_char_t *name = xmlGetNsProp(chld, "name", NS_I18N); if (name) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "Extracting element node %s.", name); i18n_translate_node(param, chld); frag_write(rtn, "${", 2); frag_write(rtn, name, strlen(name)); frag_write(rtn, "}", 1); apr_hash_set(tr_chld_nd, name, APR_HASH_KEY_STRING, chld); } else { ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, param->r_log, "Child node without i18n name found in translate."); } break; } default: break; } } // XXX: Normalize Whitespace const apr_off_t len = frag_length(rtn); xml_char_t *buf = apr_palloc(pool, len + 1); frag_to_buffer(rtn, 0, buf, len); buf[len] = '\0'; i18n_normalize_ws(buf, buf); return buf; } static xml_char_t *i18n_normalize_ws(xml_char_t * dest, const xml_char_t * src) { // remove leading blank int was_space = 1; while (*src) { if (!apr_isspace(*src)) { *dest++ = *src; was_space = 0; } else if (!was_space) { *dest++ = ' '; was_space = 1; } ++src; } if (was_space) // remove trailing blank *(--dest) = 0; else *dest = 0; return (dest); } // This does not work yet // dgettext_l(domain, msgid, mylocale) static const xml_char_t *i18n_gettext(param_transform * param, const char *msgid) { sax_check_pool(param->r_log->pool); bindtextdomain(param->domain, param->locale_dir); bind_textdomain_codeset(param->domain, "UTF-8"); char *oldlocale = setlocale(LC_MESSAGES, NULL); setlocale(LC_MESSAGES, param->locale); /* uselocale would be threadsafe, but does not work locale_t oldloc = uselocale(NULL); locale_t newloc = newlocale(LC_MESSAGES_MASK, param->domain, NULL); uselocale(newloc); */ const char *rtn = dgettext(param->domain, msgid); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "i18n_gettext called for '%s' and '%s' from '%s':'%s', found '%s'.", param->domain, param->locale, param->locale_dir, msgid, rtn); /* uselocale(oldloc); //freelocale(newloc); */ setlocale(LC_MESSAGES, oldlocale); return rtn; } /* static const xml_char_t *i18n_gettext(param_transform * param, const char *msgid) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, param->r_log, "i18n_gettext called for %s.", msgid); return "Dummy translation"; } */ /***************************************************************************** * Module Handlers *****************************************************************************/ static unq_set_t *unq_locale = NULL; // only used to unify locales static apr_pool_t *p_ul = NULL; static void i18n_child_init(apr_pool_t * p, server_rec * s) { p_ul = p; unq_locale = apr_table_make(p, 10); } // BAUSTELE const int L_BUF_SZ = 20; static const xml_char_t *i18n_locale_from_lang(const char *lang) { char locale[L_BUF_SZ + 1]; if (!lang) { return NULL; } strncpy(locale, lang, L_BUF_SZ); locale[L_BUF_SZ] = '\0'; // Replace - with _ char *p = strchr(locale, '-'); if (p) { *p = '_'; } return sax_unify_name(p_ul, unq_locale, locale); } /* * i18n_filter_init */ static int i18n_filter_init(ap_filter_t * f) { // Workaround: filter_init can get multiple calls. if (f->ctx) return OK; apr_pool_t *pool = f->r->pool; i18n_cfg *cfg = ap_get_module_config(f->r->per_dir_config, &i18n_module); i18n_ctx *fctx = f->ctx = apr_pcalloc(pool, sizeof(i18n_ctx)); memset(fctx, 0, sizeof(i18n_ctx)); // fctx->starts = sax_start_stack_init(pool); fctx->domains = apr_array_make(pool, 5, sizeof(i18n_tag_entry)); fctx->unq_domain = apr_table_make(pool, 5); if (cfg->lang_param_force) { // target attributes are ignored fctx->targets = NULL; fctx->unq_target = NULL; } else { fctx->targets = apr_array_make(pool, 5, sizeof(i18n_tag_entry)); fctx->unq_target = apr_table_make(pool, 5); } fctx->bb_out = apr_brigade_create(pool, f->c->bucket_alloc); ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, "i18n_filter_init called."); xml2_tree_log_filter_chain(APLOG_MARK, f); param_transform *param = fctx->param = apr_pcalloc(pool, sizeof(param_transform)); apr_pool_create(¶m->pool, pool); apr_pool_tag(param->pool, "i18n-param"); param->r_log = f->r; param->domain = NULL; // Fill locale dir. from config param->locale_dir = cfg->locale_dir; // Read the language from the request apr_table_t *vars = apr_table_make(pool, 6); if (f->r->args && apreq_parse_query_string(pool, vars, f->r->args) != APR_SUCCESS) { ap_log_rerror(APLOG_MARK, APLOG_WARNING, 0, f->r, "Failed to parse query string."); } // BAUSTELLE const char *locale = i18n_locale_from_lang(apr_table_get(vars, cfg->lang_param)); param->locale = locale; fctx->f_tr = transform_filter_create(f, NULL, i18n_transform, param); sax_check_pool(param->r_log->pool); return OK; } /** * The desired filter chain is as follows: * 1. Streaming XPath for @i18n:* * 2. Attribute switch * 2.a Tree transform for everything inside i18n::translate * 2.b i18n actions for the rest * */ /* * i18n_filter */ static int i18n_filter(ap_filter_t * f, apr_bucket_brigade * bb) { apr_bucket *b; apr_status_t rv = APR_SUCCESS; i18n_ctx *fctx = f->ctx; apr_bucket_brigade *bb_out = fctx->bb_out; apr_pool_t *pool = f->r->pool; const xml_char_t *unq_i18n_ns = NULL; const xml_char_t *unq_translate = NULL; ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, "i18n_filter called."); xml2_tree_log_filter_chain(APLOG_MARK, f); transform_filter_y_connect(fctx->f_tr, f); // All buckets are moved to bb_out. bb_out is always passed // before leaving the filter. It is only stored in the brigade for // reuse. for (b = APR_BRIGADE_FIRST(bb); !APR_BRIGADE_EMPTY(bb); b = APR_BRIGADE_FIRST(bb)) { // There is no EOS handling sax_check_pool(fctx->param->r_log->pool); if (!BUCKET_IS_SAX(b)) { APR_BUCKET_REMOVE(b); APR_BRIGADE_INSERT_TAIL(bb_out, b); continue; } ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, "sax bucket found."); bucket_sax *bs = b->data; // Initialisations that can not be done in i18n_filter_init if (!fctx->sax) { // We need the first sax bucket, // so he have to do this here. // Initialize the current filter sax bucket. fctx->sax = apr_pcalloc(f->r->pool, sizeof(sax_ctx)); sax_ctx_init_again(fctx->sax, bs->bctx, bs->mctx, bb_out, f, NULL); // Initialize the transform filters sax bucket. // Note that bb_out, f will not be used tfor this filter. transform_filter_set_sax(fctx->f_tr, fctx->sax); } if (!unq_i18n_ns) { all_unq_t unq = fctx->sax->bctx.unq; unq_i18n_ns = sax_unify_name(pool, unq.uri, NS_I18N); unq_translate = sax_unify_name(pool, unq.name, "translate"); } if (sax_inspect_which(b) == XML_DECL) { apr_bucket_copy(b, &fctx->b_xml_decl); } ap_assert(bb_out == fctx->sax->bb); // i18n domain if (sax_inspect_which(b) == START_ELT) { const xml_char_t *domain = sax_pop_attr(b, "domain", NS_I18N, NULL); if (domain) { i18n_tag_entry *dm = apr_array_push(fctx->domains); dm->tag_value = sax_unify_name(pool, fctx->unq_domain, domain); dm->se_id = sax_inspect_se_id(b); // Set the filters domain parameter fctx->param->domain = dm->tag_value; } const xml_char_t *target = sax_pop_attr(b, "target", NS_I18N, NULL); if (fctx->targets && target) { i18n_tag_entry *tg = apr_array_push(fctx->targets); tg->tag_value = sax_unify_name(pool, fctx->unq_target, target); tg->se_id = sax_inspect_se_id(b); // Set the filters target parameter fctx->param->locale = i18n_locale_from_lang(tg->tag_value); } } else { i18n_tag_entry *dm = sax_stack_top(i18n_tag_entry, fctx->domains); if (dm && (sax_inspect_se_id(b) == dm->se_id)) { apr_array_pop(fctx->domains); // Set the filters domain parameter dm = sax_stack_top(i18n_tag_entry, fctx->domains); fctx->param->domain = dm ? dm->tag_value : NULL; } if (fctx->targets) { i18n_tag_entry *tg = sax_stack_top(i18n_tag_entry, fctx->targets); if (tg && (sax_inspect_se_id(b) == tg->se_id)) { apr_array_pop(fctx->targets); // Set the filters target parameter tg = sax_stack_top(i18n_tag_entry, fctx->targets); fctx->param->locale = tg ? i18n_locale_from_lang(tg->tag_value) : NULL; } } } // i18n detection se_id_t se_id = sax_inspect_ns(b, NS_I18N, NULL, 1); if (se_id > 0) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, "i18n (start) tag."); // We check if we have entered a translate tag if (!fctx->translate) { if (sax_inspect_which(b) == START_ELT) { start_elt_t *se = sax_inspect_event(b); attr_t *attr = se->atts; while (attr = sax_extract_next_attr(attr, unq_i18n_ns, NULL), attr) { if (attr->name.uri == unq_i18n_ns) { fctx->translate = se_id; // we pass the buckets before b ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, "Passing brigade to %s.", f->next->frec->name); sax_check_pool(fctx->param->r_log->pool); rv = ap_pass_brigade(f->next, bb_out); if (rv != APR_SUCCESS) { return rv; } apr_array_header_t *namespaces = ((bucket_sax *) (b->data))->bctx->namespaces; rv = transform_start_faked_doc(fctx->sax, bb_out, fctx->b_xml_decl, namespaces); if (rv != APR_SUCCESS) { return rv; } } attr++; } } } } APR_BUCKET_REMOVE(b); APR_BRIGADE_INSERT_TAIL(bb_out, b); // We check if we are leaving a translate tag if (fctx->translate) { if (sax_inspect_se_id(b) == -fctx->translate) { fctx->translate = 0; // we pass the buckets up to and including b apr_array_header_t *namespaces = ((bucket_sax *) (b->data))->bctx->namespaces; rv = transform_end_faked_doc(fctx->sax, bb_out, namespaces); if (rv != APR_SUCCESS) { return rv; } ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, "Passing brigade to %s.", fctx->f_tr->frec->name); sax_check_pool(fctx->param->r_log->pool); rv = ap_pass_brigade(fctx->f_tr, bb_out); if (rv != APR_SUCCESS) { return rv; } } } APR_BRIGADE_CHECK_CONSISTENCY(bb); APR_BRIGADE_CHECK_CONSISTENCY(bb_out); } sax_check_pool(fctx->param->r_log->pool); // We are done, so we pass the brigade if (fctx->translate) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, "Passing incomplete brigade to %s.", fctx->f_tr->frec->name); rv = ap_pass_brigade(fctx->f_tr, bb_out); } else { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, f->r, "Passing brigade to %s.", f->next->frec->name); rv = ap_pass_brigade(f->next, bb_out); } // and cleanup // apr_brigade_cleanup(bb); /// param->pool clear - when? // apr_pool_clear(fctx->p_tmp); return rv; } /***************************************************************************** * Configuration *****************************************************************************/ static const char *i18n_locale_dir(cmd_parms * cmd, void *cf, const char *arg) { i18n_cfg *cfg = cf; // Check if arg is a valid path? cfg->locale_dir = arg; ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, cmd->pool, "locale_dir in cfg set to %s.", arg); return NULL; } static const char *i18n_lang_param(cmd_parms * cmd, void *cf, const char *arg) { i18n_cfg *cfg = cf; /* libapreq2 is 50k on linux, thats not wort the hassle. static apr_dso_handle_t *dlhandle = NULL; apr_status_t rv; if (!dlhandle) { #ifdef WIN32 const char path = "libapreq2.dll"; #else const char path = "libapreq2.dll"; #endif rv = apr_dso_load(&dlhandle, path, pool); if (rv != APR_SUCCESS) { // APR_EDSOOPEN return; } } */ cfg->lang_param = arg; ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, cmd->pool, "lang_param in cfg set to %s.", arg); return NULL; } static const char *i18n_lang_param_force(cmd_parms * cmd, void *cf, const char *ignored) { i18n_cfg *cfg = cf; cfg->lang_param_force = 1; ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, cmd->pool, "lang_param_force in cfg set."); return NULL; } static const command_rec i18n_cmds[] = { AP_INIT_TAKE1("I18NLocaleDir", i18n_locale_dir, NULL, OR_ALL, "The locale directory for gettext."), AP_INIT_TAKE1("I18NLangParam", i18n_lang_param, NULL, OR_ALL, "The name of the request parameter to set the language."), // AP_INIT_NO_ARG does not work AP_INIT_RAW_ARGS("I18NLangParamForce", i18n_lang_param_force, NULL, OR_ALL, "The language set by a request parameter takes priority."), { NULL} }; static void *cr_i18n_cfg(apr_pool_t * pool, char *x) { i18n_cfg *cfg = apr_pcalloc(pool, sizeof(i18n_cfg)); cfg->locale_dir = NULL; cfg->lang_param = NULL; cfg->lang_param_force = 0; ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, pool, "cr_i18n_cfg called with %s.", x); return cfg; } static void *merge_i18n_cfg(apr_pool_t * pool, void *BASE, void *ADD) { i18n_cfg *base = BASE; i18n_cfg *add = ADD; i18n_cfg *cfg = apr_palloc(pool, sizeof(i18n_cfg)); ap_log_perror(APLOG_MARK, APLOG_DEBUG, 0, pool, "merge_i18n_cfg called for %s.", add->locale_dir); *cfg = *base; if (add->locale_dir) { cfg->locale_dir = add->locale_dir; } if (add->lang_param) { cfg->lang_param = add->lang_param; } if (add->lang_param_force) { cfg->lang_param_force = 1; } return cfg; } /***************************************************************************** * The usual module stuff *****************************************************************************/ static void i18n_hooks(apr_pool_t * p) { ap_hook_child_init(i18n_child_init, NULL, NULL, APR_HOOK_MIDDLE); ap_register_output_filter("i18n", i18n_filter, i18n_filter_init, AP_FTYPE_RESOURCE); } module AP_MODULE_DECLARE_DATA i18n_module = { STANDARD20_MODULE_STUFF, cr_i18n_cfg, merge_i18n_cfg, NULL, NULL, i18n_cmds, i18n_hooks };