Behaviour specific to copies of the language genre.


§1. Scanning metadata. Metadata for natural languages is stored in the following structure. Inform can read and write text in multiple natural languages, though it needs help to do so: each natural language known to Inform comes from a small resource folder called its "bundle". (This includes English.)

enum PLAYED_LSUPPORT from 1
enum WRITTEN_LSUPPORT
enum INDEXED_LSUPPORT
enum REPORTED_LSUPPORT

define MAX_LSUPPORTS 5
typedef struct inform_language {
    struct inbuild_copy *as_copy;
    struct wording instance_name;  instance name, e.g., "German language"
    struct instance *nl_instance;  instance, e.g., "German language"
    struct text_stream *iso_code;  e.g., "fr" or "de"
    struct text_stream *translated_name;  e.g., "Deutsch"
    struct text_stream *native_cue;  e.g., "in deutscher Sprache"
    struct inform_extension *belongs_to;  if it does belong to an extension
    int adaptive_person;  which person text substitutions are written from
    int Preform_loaded;  has a Preform syntax definition been read for this?
    int supports[MAX_LSUPPORTS];
    CLASS_DEFINITION
} inform_language;

§2. This is called as soon as a new copy C of the language genre is created.

void Languages::scan(inbuild_copy *C) {
    inform_language *L = CREATE(inform_language);
    L->as_copy = C;
    if (C == NULL) internal_error("no copy to scan");
    Copies::set_metadata(C, STORE_POINTER_inform_language(L));
    TEMPORARY_TEXT(sentence_format)
    WRITE_TO(sentence_format, "%S language", C->edition->work->title);
    L->instance_name = Feeds::feed_text(sentence_format);
    DISCARD_TEXT(sentence_format)
    L->nl_instance = NULL;
    L->Preform_loaded = FALSE;
    L->adaptive_person = -1;  i.e., none yet specified
     these defaults should always be overwritten
    L->iso_code = I"en";
    L->translated_name = I"English";
     but not this one
    L->native_cue = NULL;
    L->belongs_to = NULL;
    for (int i=0; i<MAX_LSUPPORTS; i++) L->supports[i] = FALSE;

    filename *about_file = Filenames::in(Languages::path_to_bundle(L), I"about.txt");
    if (TextFiles::exists(about_file)) {
        TEMPORARY_TEXT(err)
        WRITE_TO(err, "a language bundle should no longer use an 'about.txt' file");
        Copies::attach_error(C, CopyErrors::new_T(METADATA_MALFORMED_CE, -1, err));
        DISCARD_TEXT(err)
    }

    filename *F = Filenames::in(C->location_if_path, I"language_metadata.json");
    if (TextFiles::exists(F)) {
        JSONMetadata::read_metadata_file(C, F, NULL, NULL);
        if (C->metadata_record) {
            JSON_value *language_details =
                JSON::look_up_object(C->metadata_record, I"language-details");
            if (language_details) Extract the language details2.1
            else {
                TEMPORARY_TEXT(err)
                WRITE_TO(err, "'language_metadata.json' must contain a \"language-details\" field");
                Copies::attach_error(C, CopyErrors::new_T(METADATA_MALFORMED_CE, -1, err));
                DISCARD_TEXT(err)
            }
            JSON_value *needs = JSON::look_up_object(C->metadata_record, I"needs");
            if (needs) {
                TEMPORARY_TEXT(err)
                WRITE_TO(err, "language bundle is not allowed to have 'needs'");
                Copies::attach_error(C, CopyErrors::new_T(METADATA_MALFORMED_CE, -1, err));
                DISCARD_TEXT(err)
            }
        }
    } else {
        SVEXPLAIN(2, "(no JSON metadata file found at %f)\n", F);
        TEMPORARY_TEXT(err)
        WRITE_TO(err, "a language bundle must now provide a 'language_metadata.json' file");
        Copies::attach_error(C, CopyErrors::new_T(METADATA_MALFORMED_CE, -1, err));
        DISCARD_TEXT(err)
    }
}

§2.1. Extract the language details2.1 =

    JSON_value *translated_name = JSON::look_up_object(language_details, I"translated-name");
    if (translated_name) L->translated_name = Str::duplicate(translated_name->if_string);

    JSON_value *iso_code = JSON::look_up_object(language_details, I"iso-639-1-code");
    if (iso_code) L->iso_code = Str::duplicate(iso_code->if_string);

    JSON_value *translated_syntax_cue = JSON::look_up_object(language_details, I"translated-syntax-cue");
    if (translated_syntax_cue) L->native_cue = Str::duplicate(translated_syntax_cue->if_string);

    JSON_value *supports = JSON::look_up_object(language_details, I"supports");
    if (supports) {
        JSON_value *E;
        LOOP_OVER_LINKED_LIST(E, JSON_value, supports->if_list) {
            text_stream *key = E->if_string;
            if (Str::eq(key, I"played")) L->supports[PLAYED_LSUPPORT] = TRUE;
            if (Str::eq(key, I"written")) L->supports[WRITTEN_LSUPPORT] = TRUE;
            if (Str::eq(key, I"indexed")) L->supports[INDEXED_LSUPPORT] = TRUE;
            if (Str::eq(key, I"reported")) L->supports[REPORTED_LSUPPORT] = TRUE;
        }
    }

§3.

pathname *Languages::path_to_bundle(inform_language *L) {
    return L->as_copy->location_if_path;
}

§4. Logging.

void Languages::log(OUTPUT_STREAM, char *opts, void *vL) {
    inform_language *L = (inform_language *) vL;
    if (L == NULL) { LOG("<null-language>"); }
    else { LOG("%S", L->as_copy->edition->work->title); }
}

§5. Language code. This is used when we write the bibliographic data for the work of IF we're making; this enables online databases like IFDB, and smart interpreters, to detect the language of play for a story file without actually running it.

void Languages::write_ISO_code(OUTPUT_STREAM, inform_language *L) {
    #ifdef CORE_MODULE
    if (L == NULL) L = DefaultLanguage::get(NULL);
    #endif
    WRITE("%S", L->iso_code);
}

§6. Support. Different languages support different levels of translation.

int Languages::supports(inform_language *L, int S) {
    if ((S<1) || (S>=MAX_LSUPPORTS)) internal_error("support out of range");
    if (L == NULL) return FALSE;
    return L->supports[S];
}

§7. Kit. Each language needs its own kit of Inter code, if it is going to be used as a language of play. The following is called only when `L` is the language of play for `project`:

void Languages::add_kit_dependencies_to_project(inform_language *L, inform_project *project) {
    if (L == NULL) internal_error("no language");
    TEMPORARY_TEXT(kitname)
    WRITE_TO(kitname, "%SLanguageKit", L->as_copy->edition->work->title);
    inbuild_work *work = Works::new_raw(kit_genre, kitname, I"");
    inbuild_requirement *req = Requirements::any_version_of(work);
    Projects::add_kit_dependency(project, kitname, L, NULL, req, NULL);
    DISCARD_TEXT(kitname)
}

§8. Finding by name. Given the name of a natural language (e.g., "German") we find the corresponding definition. That will mean searching for a copy, and that raises the question of where to look — in particular, it's important to include the Materials folder for any relevant project.

linked_list *search_list_for_Preform_callback = NULL;
int Languages::read_Preform_definition(inform_language *L, linked_list *S) {
    if (L == NULL) internal_error("no language");
    if (L->Preform_loaded == FALSE) {
        L->Preform_loaded = TRUE;
        search_list_for_Preform_callback = S;
        int n = (*shared_preform_callback)(L);
        if (n == 0) return FALSE;
    }
    return TRUE;
}

§9. This function is called only from Preform...

define PREFORM_LANGUAGE_FROM_NAME_WORDS_CALLBACK Languages::Preform_find
inform_language *Languages::Preform_find(text_stream *name) {
    return Languages::find_for(name, search_list_for_Preform_callback);
}

§10. ...but this one is more generally available.

inform_language *Languages::find_for(text_stream *name, linked_list *search) {
    text_stream *author = NULL;
    match_results mr = Regexp::create_mr();
    if (Regexp::match(&mr, name, U"(%c+) [Ll]anguage by (%c+)")) {
        name = mr.exp[0]; author = mr.exp[1];
    } else if (Regexp::match(&mr, name, U"(%c+) by (%c+)")) {
        name = mr.exp[0]; author = mr.exp[1];
    } else if (Regexp::match(&mr, name, U"(%c+) [Ll]anguage")) {
        name = mr.exp[0];
    }
    TEMPORARY_TEXT(title)
    WRITE_TO(title, "%S Language", name);
    inbuild_requirement *extension_req =
        Requirements::any_version_of(Works::new(extension_bundle_genre, title, author));
    inbuild_search_result *extension_R = Nests::search_for_best(extension_req, search);
    DISCARD_TEXT(title)
    if (extension_R) {
        inform_extension *E = Extensions::from_copy(extension_R->copy);
        inbuild_nest *N = Extensions::materials_nest(E);
        if (N) {
            linked_list *longer = NEW_LINKED_LIST(inbuild_nest);
            ADD_TO_LINKED_LIST(N, inbuild_nest, longer);
            inbuild_requirement *req =
                Requirements::any_version_of(Works::new(language_genre, name, I""));
            inbuild_search_result *R = Nests::search_for_best(req, longer);
            if (R) {
                inform_language *L = LanguageManager::from_copy(R->copy);
                L->belongs_to = E;
                Regexp::dispose_of(&mr);
                return L;
            }
        }
    }
    inbuild_requirement *req =
        Requirements::any_version_of(Works::new(language_genre, name, I""));
    inbuild_search_result *R = Nests::search_for_best(req, search);
    Regexp::dispose_of(&mr);
    if (R) return LanguageManager::from_copy(R->copy);
    return NULL;
}

§11. Or we can convert the native cue, en français, to the name, French, though this will not work for language bundles inside of extensions, and so the function is now deprecated:

text_stream *Languages::find_by_native_cue(text_stream *cue, linked_list *search) {
    SVEXPLAIN(3, "(find language by native cue %S)\n", cue);
    linked_list *results = NEW_LINKED_LIST(inbuild_search_result);
    Nests::search_for(Requirements::anything_of_genre(language_genre), search, results);
    inbuild_search_result *search_result;
    LOOP_OVER_LINKED_LIST(search_result, inbuild_search_result, results) {
        inform_language *L = LanguageManager::from_copy(search_result->copy);
        if (Str::eq_insensitive(cue, L->native_cue)) {
            SVEXPLAIN(3, "(found at %p)\n", L->as_copy->location_if_path);
            return L->as_copy->edition->work->title;
        }
    }
    SVEXPLAIN(3, "(not found)\n");
    return NULL;
}

§12. Finally, the following Preform nonterminal matches the English-language name of a language: for example, "French". Unlike the above functions, it looks only at languages already loaded, and doesn't scan nests for more.

<natural-language> internal {
    inform_language *L;
    LOOP_OVER(L, inform_language)
        if (Wordings::match(W, Wordings::first_word(L->instance_name))) {
            ==> { -, L };
            return TRUE;
        }
    ==> { fail };
}