ScriptFu: fix #12837 i18n for independent scripts

See /scripts/test/test-i18n.scm,
which has tests and documents use cases.

Some incidental refactoring and editing of comments.
This commit is contained in:
lloyd konneker 2025-02-01 15:43:00 -05:00 committed by Lloyd Konneker
parent 6dfd27b271
commit ea238e4e5a
22 changed files with 866 additions and 120 deletions

View file

@ -52,6 +52,18 @@ G_DEFINE_TYPE (ScriptFuInterpreter, script_fu_interpreter, GIMP_TYPE_PLUG_IN)
*/
static gchar * path_to_this_script;
/* For any plugin interpreted by self, these are the names, not always enforced:
* plugin file name is plugin-<foo>.scm (it has shebang)
* progname is the same (not the name of the interpreter)
* plugin name is plugin-<foo>
* PDB procedure name is plugin-<foo>
* run_func in Scheme is named plugin-<foo> (not script-fu-<foo>)
* C run func called by GIMP is e.g. script_fu_run_image_procedure
*/
/* Connect to Gimp. See libgimp/gimp.c.
*
* Can't use GIMP_MAIN macro, it doesn't omit argv[0].
@ -84,6 +96,79 @@ int main (int argc, char *argv[])
g_debug ("Exit script-fu-interpreter.");
}
/* A callback from GIMP.
* A method of GimpPlugin.
* GIMP calls often, before any phase (query, create, init, run.)
*
* It is only necessary before the create phase,
* when we declare args and menu item possibly requiring i18n.
* FUTURE: avoid this work for phases other than create and run.
*
* Since it is *before* the create phase,
* SF has not read the script and interpreted it's registration functions,
* especially a call to script-fu-register-i18n
* We must do that to get the declared i18n domain and catalog.
*/
static gboolean
script_fu_set_i18n (GimpPlugIn *plug_in,
const gchar *procedure_name,
gchar **gettext_domain,
gchar **catalog_dir)
{
gchar *declared_i18n_domain = NULL;
gchar *declared_i18n_catalog = NULL;
gboolean result;
/* assert that *gettext_domain and *catalog_dir are NULL and don't need free. */
g_debug ("%s", G_STRFUNC);
/* Get script author's declared i18n into local vars.*/
script_fu_interpreter_get_i18n ( plug_in,
procedure_name,
path_to_this_script,
&declared_i18n_domain,
&declared_i18n_catalog);
/* Convert script declared i18n keywords to other values. */
if (declared_i18n_domain == NULL ||
g_strcmp0 (declared_i18n_domain, "None") == 0)
{
/* The script has not called script-fu-register-i18n.
* OR with domain_name of "None".
* Return FALSE to mean: no translations.
*/
*gettext_domain = NULL;
result = FALSE;
}
else if ( g_strcmp0 (declared_i18n_domain, "Standard") == 0)
{
/* Script author wants default domain name and catalog.
* Set to NULL, and return TRUE tells GimpPlugin to use default.
*/
*gettext_domain = NULL;
*catalog_dir = NULL;
result = TRUE; /* want translation. */
}
else
{
/* Script author provided non-standard domain and catalog.
* Return allocated copy to caller.
*/
*gettext_domain = g_strdup (declared_i18n_domain);
*catalog_dir = g_strdup (declared_i18n_catalog);
result = TRUE; /* want translation. */
}
g_debug ("%s returns %d domain %s catalog %s",
G_STRFUNC, result, declared_i18n_domain, declared_i18n_catalog);
g_free (declared_i18n_domain);
g_free (declared_i18n_catalog);
return result;
}
static void
script_fu_interpreter_class_init (ScriptFuInterpreterClass *klass)
{
@ -92,17 +177,12 @@ script_fu_interpreter_class_init (ScriptFuInterpreterClass *klass)
plug_in_class->query_procedures = script_fu_interpreter_query_procedures;
plug_in_class->create_procedure = script_fu_interpreter_create_procedure;
/* Do not override virtual method set_i18n.
/* Override virtual method set_i18n.
* Default implementation finds translations in:
* GIMP's .../plug-ins/<plugin_name>/locale/<lang>/LC_MESSAGES/<plugin_name>.mo
*
* For any plugin interpreted by self:
* plugin file name is plugin-<foo>.scm (it has shebang)
* progname is the same (not the name of the interpreter)
* plugin name is plugin-<foo>
* PDB procedure name is plugin-<foo>
* run_func in Scheme is named plugin-<foo> (not script-fu-<foo>)
* and throws error to console when not exists.
*/
plug_in_class->set_i18n = script_fu_set_i18n;
}
@ -121,7 +201,7 @@ script_fu_interpreter_query_procedures (GimpPlugIn *plug_in)
{
GList *result = NULL;
g_debug ("queried");
g_debug ("%s", G_STRFUNC);
/* Init ui, gegl, babl.
* Need gegl in registration phase, to get defaults for color formal args.
@ -154,7 +234,5 @@ script_fu_interpreter_create_procedure (GimpPlugIn *plug_in,
*/
gimp_ui_init ("script-fu-interpreter");
return script_fu_interpreter_create_proc_at_path (plug_in,
proc_name,
path_to_this_script);
return script_fu_interpreter_create_proc (plug_in, proc_name, path_to_this_script);
}

View file

@ -43,19 +43,36 @@
* When we call script_fu_init_embedded_interpreter(),
* the passed paths should include the path to /scripts
* because that is the location of scripts for initialization and compatibility
* (script-fu.init, plug-in-compat.init and script-fu-compat.init,
* which are really scheme files.)
* e.g. init.scm.
*
* scrip-fu-interpreter always inits embedded interpreter(allow_register=TRUE)
* In the "run" phase, you don't need script-fu-register to be defined, but its harmless.
*
* The usual sequence of phases and callbacks from GimpPlugin is:
* query phase
* set_i18n script_fu_interpreter_get_i18n
* query procedures script_fu_interpreter_list_defined_proc_names
* run phase
* set_i18n script_fu_interpreter_get_i18n
* create procedure script_fu_interpreter_create_proc
* set_i18n script_fu_interpreter_get_i18n
* run procedure (calls directly a C func defined in script-fu-run-func.c)
* We only init interpreter and load scripts once per phase.
*/
static GFile *script_fu_get_plugin_parent_path (const gchar *path_to_this_script);
static void script_fu_free_path_list (GList **list);
static void script_fu_interpreter_init_inner (void);
static void script_fu_interpreter_load_script (
GimpPlugIn *plug_in,
const gchar *path_to_script);
static void script_fu_interpreter_init_and_load_script (
GimpPlugIn *plug_in,
const gchar *path_to_script);
/* Return a list of PDB procedure names defined in all .scm files in
* the parent dir of the given path, which is a filename of the one being queried.
* This is called in the "query" phase, and subsequently the interpreter will exit.
*
* Each .scm file may contain many calls to script-fu-register, which defines a PDB procedure.
* All .scm files in the parent dir are searched.
@ -74,14 +91,9 @@ script_fu_interpreter_list_defined_proc_names (GimpPlugIn *plug_in,
GList *name_list = NULL; /* list of strings */
GList *path_list = NULL; /* list of GFile */
/* path_list is /scripts dir etc. from which we will load compat and init scripts.
* second argument TRUE means define script-fu-register into the interpreter.
*/
path_list = script_fu_search_path ();
script_fu_init_embedded_interpreter (path_list, TRUE, GIMP_RUN_NONINTERACTIVE, FALSE);
script_fu_free_path_list (&path_list);
script_fu_interpreter_init_inner();
/* Reuse path_list, now a list of one path, the parent dir of the queried script. */
/* List one path, the parent dir of the queried script. */
path_list = g_list_append (path_list,
script_fu_get_plugin_parent_path (path_to_this_script));
name_list = script_fu_find_scripts_list_proc_names (plug_in, path_list);
@ -93,31 +105,14 @@ script_fu_interpreter_list_defined_proc_names (GimpPlugIn *plug_in,
return name_list;
}
/* Create a PDB proc of type PLUGIN with the given name.
* Unlike extension-script-fu, create proc of type PLUGIN.
*
* We are in "create procedure" phase of call from GIMP.
* Create a PDB procedure that the script-fu-interpreter wraps.
*
* A GimpPDBProcedure has a run function, here script_fu_script_proc()
* of this outer interpreter.
* Sometime after the create, GIMP calls the run func, passing a name aka command.
* In ScriptFu, the same name is used for the PDB proc and the Scheme function
* which is the inner run func defined in the script.
* script_fu_script_proc calls the TinyScheme interpreter to evaluate
* the inner run func in the script.
*/
GimpProcedure *
script_fu_interpreter_create_proc_at_path (GimpPlugIn *plug_in,
const gchar *proc_name,
const gchar *path_to_this_script
)
script_fu_interpreter_create_proc (GimpPlugIn *plug_in,
const gchar *proc_name,
const gchar *path_to_script)
{
GimpProcedure *procedure = NULL;
GList *path_list = NULL; /* list of GFile */
g_debug ("script_fu_interpreter_create_proc_at_path, name: %s", proc_name);
g_debug ("%s name: %s", G_STRFUNC, proc_name);
/* Require proc_name is a suitable name for a PDB procedure eg "script-fu-test".
* (Not tested for canonical name "script-fu-<something>")
@ -130,35 +125,118 @@ script_fu_interpreter_create_proc_at_path (GimpPlugIn *plug_in,
* Otherwise, we simply won't find the proc_name defined in any .scm file,
* and will fail gracefully, returning NULL.
*/
path_list = script_fu_search_path ();
path_list = g_list_append (path_list,
script_fu_get_plugin_parent_path (path_to_this_script));
/* path_list are the /scripts dir, for .init and compat.scm, plus the path to this.
* second arg TRUE means define script-fu-register so it is effective.
/* Load scripts.
* We loaded scripts prior for callback gimp_plugin_set_18n,
* but then i18n was not in effect.
* Load again, but now i18n will translate GUI strings for declared args.
*/
script_fu_init_embedded_interpreter (path_list, TRUE, GIMP_RUN_NONINTERACTIVE, FALSE);
script_fu_interpreter_load_script (plug_in, path_to_script);
/* Reuse path_list, now a list of only the path to this script. */
script_fu_free_path_list (&path_list);
path_list = g_list_append (path_list,
script_fu_get_plugin_parent_path (path_to_this_script));
/* Assert loaded scripts has proc_name. Else this fails, returns NULL. */
procedure = script_fu_create_PDB_proc_plugin (plug_in, proc_name);
procedure = script_fu_find_scripts_create_PDB_proc_plugin (plug_in, path_list, proc_name);
script_fu_free_path_list (&path_list);
/* When procedure is not NULL, assert:
* some .scm was evaluated.
* the script defined many PDB procedures locally, i.e. in script-tree
* we created a single PDB procedure (but not put it in the GIMP PDB)
*
* Ensure procedure is-a GimpProcedure or NULL.
* GIMP is the caller and will put non-NULL procedure in the PDB.
*/
/* When procedure is not NULL, the caller GIMP will it in the PDB. */
return procedure;
}
/* Return i18n domain and catalog declared by script at path.
* Returns NULL when script did not call script-fu-register-i18n.
* Returned strings are new allocated.
* Returned strings are returned at the given handles.
*
* This is only used by the interpreter.
* extension-script-fu does not let old-style scripts in /scripts
* declare i18n; such scripts use only the shared translations gimp30-script-fu.mo.
*
* This is called twice when GIMP is running the procedure:
* 1. before create
* 2. before run
* For the first call, we initialize the interpreter and load the script.
* For the second call, the script is still loaded,
* and we return the same results as the first call.
*/
void
script_fu_interpreter_get_i18n (GimpPlugIn *plug_in,
const gchar *proc_name,
const gchar *path_to_this_script,
gchar **i18n_domain, /* OUT handles */
gchar **i18n_catalog)
{
/* As discussed above, this may be called many times in same interpreter session.
* Only init interpreter once.
*/
if (! script_fu_is_scripts_loaded ())
{
script_fu_interpreter_init_and_load_script (plug_in, path_to_this_script);
}
/* This will allocate strings and set the handles. */
return script_fu_get_i18n_for_proc (proc_name, i18n_domain, i18n_catalog);
}
/* Load plugin .scm files from directory at path.
* Side effects on the interpreter's tree of scripts.
*
* This may load many files which define many PDB procedures.
* This does not install the PDB procedures defined by the scripts.
*
* This can be called sequentially but the effect is not cumulative:
* it frees any scripts already loaded into internal tree.
* A second call reloads the tree.
*/
static void
script_fu_interpreter_load_script (GimpPlugIn *plug_in,
const gchar *path_to_script)
{
GList *path_list = NULL; /* list of GFile */
/* Convert file name to list of one parent path.
* A SF independently interpreted file must be in its own dir,
* and load_scripts wants a dir, not a file.
*/
path_list = g_list_append (path_list,
script_fu_get_plugin_parent_path (path_to_script));
/* Get scripts into global state: scripts_tree. */
(void) script_fu_load_scripts_into_tree (plug_in, path_list);
script_fu_free_path_list (&path_list);
/* Not ensure script_fu_is_scripts_loaded(),
* when the path is bad or is to a file that is not a valid SF script file.
* Not ensure that the file declared any particular procedure name.
*/
}
/* Init the SF interpreter and load plugin script files at path.
*
* Is an error to call more than once.
*
* The path is to a directory expected to contain one or more plugin .scm file.
*
* Initting the interpreter also "loads" non-plugin .scm files,
* where "load" means in Scheme: read and evaluate.
*/
/* FUTURE we should not need to pass plug_in. See script_fu_remove_script */
static void
script_fu_interpreter_init_and_load_script (GimpPlugIn *plug_in,
const gchar *path_to_script)
{
if (script_fu_is_scripts_loaded ())
{
g_error ("%s interpreter already init", G_STRFUNC);
return;
}
script_fu_interpreter_init_inner();
script_fu_interpreter_load_script (plug_in, path_to_script);
}
/* Return GFile of the parent directory of this plugin, whose filename is given.
*
* Caller must free the GFile.
@ -190,3 +268,41 @@ script_fu_free_path_list (GList **list)
/* !!! g_steal_pointer takes a handle. */
g_list_free_full (g_steal_pointer (list), g_object_unref);
}
/* Init the TinyScheme interpreter
* and the ScriptFu interpreter that wraps it.
*
* Side effects only, on the state of the interpreter.
*
* Ensures:
* Innermost TinyScheme interpreter is initialized.
* It has loaded init.scm (and some other scripts in /scripts/init)
* The ScriptFu registration functions are defined
* (and other functions unique to ScriptFu outer interpreter.)
*
* It has NOT loaded plugin scripts:
* in /scripts, served by extension-script-fu
* in /plug-ins, served by independent interpreter
*/
static void
script_fu_interpreter_init_inner (void)
{
GList *path_list = NULL; /* list of GFile */
g_debug ("%s", G_STRFUNC);
path_list = script_fu_search_path ();
/* path_list is /scripts dir which has subdir /init of compat and init scripts. */
/* Second argument TRUE means define script-fu-register
* and other registration functions into the interpreter.
* So that plugin scripts WILL load IN THE FUTURE.
* This does not load any plugins,
* but subsequently, the interpreter will recognize registration functions
* when interpreter loads a plugin .scm file.
*
* Fourth argument FALSE means use no progress reporting.
*/
script_fu_init_embedded_interpreter (path_list, TRUE, GIMP_RUN_NONINTERACTIVE, FALSE);
script_fu_free_path_list (&path_list);
}

View file

@ -21,9 +21,15 @@
GList *script_fu_interpreter_list_defined_proc_names (
GimpPlugIn *plug_in,
const gchar *path_to_this_plugin);
GimpProcedure *script_fu_interpreter_create_proc_at_path (
GimpProcedure *script_fu_interpreter_create_proc (
GimpPlugIn *plug_in,
const gchar *proc_name,
const gchar *path_to_this_script);
const gchar *path_to_script);
void script_fu_interpreter_get_i18n (
GimpPlugIn *plug_in,
const gchar *proc_name,
const gchar *path_to_this_script,
gchar **i18n_domain,
gchar **i18n_catalog);
#endif /* __SCRIPT_FU_INTERPRETER_H__ */

View file

@ -112,6 +112,8 @@ static pointer script_fu_register_call_procedure (scheme
pointer a);
static pointer script_fu_menu_register_call (scheme *sc,
pointer a);
static pointer script_fu_register_i18n_call (scheme *sc,
pointer a);
static pointer script_fu_use_v3_call (scheme *sc,
pointer a);
static pointer script_fu_use_v2_call (scheme *sc,
@ -553,6 +555,7 @@ ts_define_procedure (sc, "load-extension", scm_load_ext);
ts_define_procedure (sc, "script-fu-register-filter", script_fu_register_call_filter);
ts_define_procedure (sc, "script-fu-register-procedure", script_fu_register_call_procedure);
ts_define_procedure (sc, "script-fu-menu-register", script_fu_menu_register_call);
ts_define_procedure (sc, "script-fu-register-i18n", script_fu_register_i18n_call);
}
else
{
@ -560,6 +563,7 @@ ts_define_procedure (sc, "load-extension", scm_load_ext);
ts_define_procedure (sc, "script-fu-register-filter", script_fu_nil_call);
ts_define_procedure (sc, "script-fu-register-procedure", script_fu_nil_call);
ts_define_procedure (sc, "script-fu-menu-register", script_fu_nil_call);
ts_define_procedure (sc, "script-fu-register-i18n", script_fu_nil_call);
}
ts_define_procedure (sc, "script-fu-use-v3", script_fu_use_v3_call);
@ -2346,6 +2350,13 @@ script_fu_menu_register_call (scheme *sc,
return script_fu_add_menu (sc, a);
}
static pointer
script_fu_register_i18n_call (scheme *sc,
pointer a)
{
return script_fu_add_i18n (sc, a);
}
static pointer
script_fu_use_v3_call (scheme *sc,
pointer a)

View file

@ -25,6 +25,7 @@
#include "script-fu-types.h" /* SFScript */
#include "scheme-wrapper.h" /* tinyscheme_init etc, */
#include "script-fu-scripts.h" /* script_fu_find_scripts */
#include "script-fu-script.h" /* script_fu_script_get_i18n */
#include "script-fu-interface.h" /* script_fu_interface_is_active */
#include "script-fu-proc-factory.h"
@ -105,21 +106,38 @@ script_fu_init_embedded_interpreter (GList *paths,
tinyscheme_init (paths, allow_register);
ts_set_run_mode (run_mode);
/*
* Ensure the embedded interpreter is running
* and has loaded its internal Scheme scripts
* and has defined existing PDB procs as Scheme foreign functions
* (is ready to interpret PDB-like function calls in scheme scripts.)
* Ensure the embedded interpreter is running and:
* loaded its internal Scheme scripts e.g. init.scm
* defined existing PDB procs as Scheme foreign functions
* (is ready to interpret PDB-like function calls in scheme scripts.)
* has loaded other init and compat scripts in /scripts/init
* e.g. script-fu-compat.scm
*
* scripts/...init and scripts/...compat.scm are loaded
* iff paths includes the "/scripts" dir.
*
* The .scm file(s) for plugins are loaded
* iff paths includes their parent directory (e.g. /scripts)
* Loaded does not imply yet registered in the PDB
* (yet, they soon might be for some phases of the plugin.)
* The .scm file(s) for plugins in /scripts are NOT loaded.
* Any util scripts in /scripts are NOT loaded, e.g. script-fu-utils.scm.
*/
}
/* Load script files at paths.
* Side effect: create state script_tree.
* Requires interpreter initialized.
*/
void
script_fu_load_scripts_into_tree (GimpPlugIn *plugin,
GList *paths)
{
script_fu_scripts_load_into_tree (plugin, paths);
}
/* Has the interpreter been initialized and a script loaded
* i.e. interpreted for registration into interpreter state: script_tree.
*/
gboolean
script_fu_is_scripts_loaded (void)
{
return script_fu_scripts_are_loaded ();
}
void
script_fu_set_print_flag (gboolean should_print)
{
@ -337,13 +355,16 @@ script_fu_is_init_directory (GFile *dir)
return result;
}
/* Create a PDB procedure from the SFScript for the given proc name.
* Does not register into the PDB.
* Requires scripts already loaded i.e. SFScript exist.
*/
GimpProcedure *
script_fu_find_scripts_create_PDB_proc_plugin (GimpPlugIn *plug_in,
GList *paths,
const gchar *name)
script_fu_create_PDB_proc_plugin (GimpPlugIn *plug_in,
const gchar *name)
{
/* Delegate to factory. */
return script_fu_proc_factory_make_PLUGIN (plug_in, paths, name);
return script_fu_proc_factory_make_PLUGIN (plug_in, name);
}
GList *
@ -353,3 +374,13 @@ script_fu_find_scripts_list_proc_names (GimpPlugIn *plug_in,
/* Delegate to factory. */
return script_fu_proc_factory_list_names (plug_in, paths);
}
/* Requires scripts already loaded. */
void
script_fu_get_i18n_for_proc (const gchar *proc_name,
gchar **declared_i18n_domain,
gchar **declared_i18n_catalog)
{
SFScript *script = script_fu_find_script (proc_name);
script_fu_script_get_i18n (script, declared_i18n_domain, declared_i18n_catalog);
}

View file

@ -36,6 +36,9 @@ void script_fu_init_embedded_interpreter (GList *paths,
gboolean allow_register,
GimpRunMode run_mode,
gboolean report_progress);
void script_fu_load_scripts_into_tree (GimpPlugIn *plugin,
GList *paths);
gboolean script_fu_is_scripts_loaded (void);
void script_fu_set_print_flag (gboolean should_print);
void script_fu_redirect_output_to_gstr (GString *output);
@ -52,10 +55,14 @@ void script_fu_run_read_eval_print_loop (void);
void script_fu_register_quit_callback (void (*func) (void));
void script_fu_register_post_command_callback (void (*func) (void));
GimpProcedure *script_fu_find_scripts_create_PDB_proc_plugin (GimpPlugIn *plug_in,
GList *paths,
const gchar *name);
GList *script_fu_find_scripts_list_proc_names (GimpPlugIn *plug_in,
GList *paths);
GimpProcedure *script_fu_create_PDB_proc_plugin (GimpPlugIn *plug_in,
const gchar *name);
void script_fu_get_i18n_for_proc (const gchar *proc_name,
gchar **declared_i18n_domain,
gchar **declared_i18n_catalog);
#endif /* __SCRIPT_FU_LIB_H__ */

View file

@ -50,34 +50,22 @@ static void script_fu_add_menu_to_procedure (GimpProcedure *procedure,
/* Create and return a single PDB procedure of type PLUGIN,
* for the given proc name, by reading the script file in the given paths.
* for the given proc name, from script_tree already loaded.
* Also add a menu for the procedure.
*
* PDB proc of type PLUGIN has permanent lifetime, unlike type TEMPORARY.
*
* The list of paths is usually just one directory, a subdir of /plug-ins.
* The directory may contain many .scm files.
* The plugin manager only queries one .scm file,
* having the same name as its parent dir and and having execute permission.
* But here we read all the .scm files in the directory.
* Each .scm file may register (and define run func for) many PDB procedures.
*
* Here, one name is passed, and though we load all the .scm files,
* we only create a PDB procedure for the passed name.
* Loaded .scm file(s) may have defined many procedures.
* Create a PDB procedure only for the one passed name.
*/
GimpProcedure *
script_fu_proc_factory_make_PLUGIN (GimpPlugIn *plug_in,
GList *paths,
const gchar *proc_name)
{
SFScript * script = NULL;
GimpProcedure * procedure = NULL;
/* Reads all .scm files at paths, even though only one is pertinent.
* The returned script_tree is also in the state of the interpreter,
* we don't need the result here.
*/
(void) script_fu_find_scripts_into_tree (plug_in, paths);
/* Require SFScripts already defined, one or more. */
/* Get the pertinent script from the tree. */
script = script_fu_find_script (proc_name);
@ -138,7 +126,7 @@ script_fu_proc_factory_list_names (GimpPlugIn *plug_in,
GTree * script_tree = NULL;
/* Load (eval) all .scm files in all dirs in paths. */
script_tree = script_fu_find_scripts_into_tree (plug_in, paths);
script_tree = script_fu_scripts_load_into_tree (plug_in, paths);
/* Iterate over the tree, adding each script name to result list */
g_tree_foreach (script_tree,

View file

@ -19,7 +19,6 @@
#define __SCRIPT_FU_PDB_PROC_FACTORY_H__
GimpProcedure *script_fu_proc_factory_make_PLUGIN (GimpPlugIn *plug_in,
GList *paths,
const gchar *name);
GList *script_fu_proc_factory_list_names (GimpPlugIn *plug_in,
GList *paths);

View file

@ -80,6 +80,8 @@ script_fu_script_new (const gchar *name,
script->copyright = g_strdup (copyright);
script->date = g_strdup (date);
script->image_types = g_strdup (image_types);
script->i18n_domain_name = NULL;
script->i18n_catalog_relative_path = NULL;
script->n_args = n_args;
script->args = g_new0 (SFArg, script->n_args);
@ -103,6 +105,8 @@ script_fu_script_free (SFScript *script)
g_free (script->copyright);
g_free (script->date);
g_free (script->image_types);
g_free (script->i18n_domain_name);
g_free (script->i18n_catalog_relative_path);
for (i = 0; i < script->n_args; i++)
{
@ -746,3 +750,35 @@ script_fu_script_get_is_old_style (SFScript *script)
{
return script->is_old_style;
}
/* Set script's i18n from strings owned by inner interpreter.
* First free any existing data since might have been set already:
* a script author may mistakenly call script-fu-register-i18n twice
* for the same procedure.
*/
void
script_fu_script_set_i18n (SFScript *script,
gchar *domain,
gchar *catalog)
{
g_free (script->i18n_domain_name);
g_free (script->i18n_catalog_relative_path);
script->i18n_domain_name = g_strdup (domain);
script->i18n_catalog_relative_path = g_strdup (catalog);
}
/* Return a copy of script's i18n, to the handles.
*
* Require *handles is NULL, not already allocated.
*
* May return NULL, when script author has not called script-fu-register-i18n.
*/
void
script_fu_script_get_i18n (SFScript *script,
gchar **domain,
gchar **catalog)
{
*domain = g_strdup (script->i18n_domain_name);
*catalog = g_strdup (script->i18n_catalog_relative_path);
}

View file

@ -65,4 +65,11 @@ void script_fu_script_set_drawable_arity_none (SFScript *scrip
void script_fu_script_set_is_old_style (SFScript *script);
gboolean script_fu_script_get_is_old_style (SFScript *script);
void script_fu_script_set_i18n (SFScript *script,
gchar *domain,
gchar *catalog);
void script_fu_script_get_i18n (SFScript *script,
gchar **domain,
gchar **catalog);
#endif /* __SCRIPT_FU_SCRIPT__ */

View file

@ -73,10 +73,34 @@ static GList *script_menu_list = NULL;
* Function definitions
*/
/* A method on the internal tree of scripts.
*
* Uninstall any PDB procedures declared by the scripts,
* and free the tree of scripts.
*
* For some phases of the plugin protocol
* the PDB procedures declared by the scripts were not installed.
* In other words, uninstall is a try that may have no effect.
*/
static void
script_fu_scripts_clear_tree ( GimpPlugIn *plug_in)
{
if (script_tree != NULL)
{
g_tree_foreach (script_tree,
(GTraverseFunc) script_fu_remove_script,
plug_in);
g_tree_destroy (script_tree);
}
}
/* Traverse list of paths, finding .scm files.
* Load and eval any found script texts.
* Script texts will call Scheme functions script-fu-register
* and script-fu-menu-register,
* Script texts will call ScriptFu registration functions
* e.g. script-fu-register and script-fu-menu-register,
* which insert a SFScript record into script_tree,
* and insert a SFMenu record into script_menu_list.
* These are side effects on the state of the outer (SF) interpreter.
@ -85,20 +109,29 @@ static GList *script_menu_list = NULL;
* The other result (script_menu_list) is not returned, see script_fu_get_menu_list.
*
* Caller should free script_tree and script_menu_list,
* This should only be called once.
* but we usually don't, the interpreter just exits.
*
* This can be called more than once but does not accumulate into the tree:
* it clears the tree on every call before reloading it.
*
* When we load plugin script files (.scm)
* the list of paths can be just one directory,
* a plugin's subdir of /plug-ins (independent interpreter).
* The plugin manager only queries one .scm file,
* having the same name as its parent dir and and having execute permission.
*
* But the list of paths can be many:
* the sys and user /scripts dirs(extension-script-fu).
*
* Any dir in the paths may contain many .scm files.
* We read all the .scm files in the directory.
* Each .scm file may register (and define run func for) many PDB procedures.
*/
GTree *
script_fu_find_scripts_into_tree ( GimpPlugIn *plug_in,
script_fu_scripts_load_into_tree ( GimpPlugIn *plug_in,
GList *paths)
{
/* Clear any existing scripts */
if (script_tree != NULL)
{
g_tree_foreach (script_tree,
(GTraverseFunc) script_fu_remove_script,
plug_in);
g_tree_destroy (script_tree);
}
script_fu_scripts_clear_tree (plug_in);
script_tree = g_tree_new ((GCompareFunc) g_utf8_collate);
@ -138,7 +171,7 @@ void
script_fu_find_scripts (GimpPlugIn *plug_in,
GList *path)
{
script_fu_find_scripts_into_tree (plug_in, path);
script_fu_scripts_load_into_tree (plug_in, path);
/* Now that all scripts are read in and sorted, tell gimp about them */
g_tree_foreach (script_tree,
@ -335,6 +368,97 @@ script_fu_add_menu (scheme *sc,
return sc->NIL;
}
/* For a call to script-fu-register-i18n,
* marshall scheme values into local SFScript struct.
*
* Returns sc->NIL on success, else a foreign_error.
* Many kinds of failure will not prevent the plugin from registering and working,
* only prevent the plugin from being translated properly.
*
* Although the set_18n callback is called many times in the same interpreter session,
* before create proc and run proc, the script is only interpreted for it's registrations once.
* However, an ill-formed script can call script-fu-register-i18n
* many times for the same procedure, see below.
*/
pointer
script_fu_add_i18n (scheme *sc,
pointer a)
{
SFScript *script;
const gchar *proc_name;
gchar *i18n_domain = NULL;
gchar *i18n_catalog_relative_path = NULL;
g_debug ("%s", G_STRFUNC);
/* Check arg count */
if (sc->vptr->list_length (sc, a) < 2)
return foreign_error (sc, "script-fu-register-i18n takes two or three args", 0);
/* PDB procedure name. */
if (sc->vptr->is_string (sc->vptr->pair_car (a)))
{
proc_name = sc->vptr->string_value (sc->vptr->pair_car (a));
a = sc->vptr->pair_cdr (a);
}
else
{
return foreign_error (sc, "script-fu-register-i18n requires first arg is string script name", 0);
}
script = script_fu_find_script (proc_name);
if (! script)
return foreign_error (sc, "script-fu-register-i18n called with invalid procedure name", 0);
/* Not an error to interpret script-fu-register-i18n twice for the same procedure.
* When there are two calls to script-fu-register-i18n for the same procedure
* in one script, the latter will have effect.
*/
if (script->i18n_domain_name != NULL || script->i18n_catalog_relative_path != NULL)
g_warning ("%s called twice for same procedure %s", G_STRFUNC, proc_name);
/* i18n domain name */
if (sc->vptr->is_string (sc->vptr->pair_car (a)))
{
i18n_domain = sc->vptr->string_value (sc->vptr->pair_car (a));
a = sc->vptr->pair_cdr (a);
}
else
{
return foreign_error (sc, "script-fu-register-i18n requires second arg is string domain name", 0);
}
/* optional catalog path */
if (a != sc->NIL)
{
if (sc->vptr->is_string (sc->vptr->pair_car (a)))
{
i18n_catalog_relative_path = sc->vptr->string_value (sc->vptr->pair_car (a));
a = sc->vptr->pair_cdr (a);
}
else
{
return foreign_error (sc, "script-fu-register-i18n requires optional third arg is catalog path", 0);
}
}
/* Call setter from local vars, strings owned by inner interpreter TS. */
script_fu_script_set_i18n (script, i18n_domain, i18n_catalog_relative_path);
return sc->NIL; /* success */
}
/* Have one or more SFScript (global data structs) been created?
* i.e. one or more script files loaded, i.e. interpreted for their registration functions.
* Returns a state of the interpreter.
*/
gboolean
script_fu_scripts_are_loaded (void)
{
return (script_tree != NULL);
}
/* private functions */
@ -478,8 +602,11 @@ script_fu_install_menu (SFMenu *menu)
g_slice_free (SFMenu, menu);
}
/*
* The following function is a GTraverseFunction.
/* Traverse list of scripts, uninstalling from PDB and
* freeing the script data.
* Then free the list, now empty of content.
*
* This function has type GTraverseFunction.
*/
static gboolean
script_fu_remove_script (gpointer foo G_GNUC_UNUSED,
@ -489,6 +616,8 @@ script_fu_remove_script (gpointer foo G_GNUC_UNUSED,
GimpPlugIn *plug_in = data;
GList *list;
g_debug ("%s", G_STRFUNC);
for (list = scripts; list; list = g_list_next (list))
{
SFScript *script = list->data;

View file

@ -28,9 +28,12 @@ pointer script_fu_add_script_regular (scheme *sc,
pointer a);
pointer script_fu_add_menu (scheme *sc,
pointer a);
pointer script_fu_add_i18n (scheme *sc,
pointer a);
GTree * script_fu_find_scripts_into_tree (GimpPlugIn *plug_in,
GTree * script_fu_scripts_load_into_tree (GimpPlugIn *plug_in,
GList *path);
gboolean script_fu_scripts_are_loaded (void);
SFScript * script_fu_find_script (const gchar *name);
GList * script_fu_get_menu_list (void);

View file

@ -88,6 +88,8 @@ typedef struct
gchar *copyright;
gchar *date;
gchar *image_types;
gchar *i18n_domain_name;
gchar *i18n_catalog_relative_path;
gint n_args;
SFArg *args;

View file

@ -15,5 +15,8 @@ EXPORTS
script_fu_register_quit_callback
script_fu_register_post_command_callback
script_fu_search_path
script_fu_find_scripts_create_PDB_proc_plugin
script_fu_create_PDB_proc_plugin
script_fu_find_scripts_list_proc_names
script_fu_is_scripts_loaded
script_fu_load_scripts_into_tree
script_fu_get_i18n_for_proc

View file

@ -189,3 +189,6 @@
(script-fu-menu-register "script-fu-test-sphere-v3"
"<Image>/Filters/Development/Plug-In Examples")
; Use the translations data common to all Scheme plugins distributed with GIMP.
(script-fu-register-i18n "script-fu-test-sphere-v3" "gimp30-script-fu" )

View file

@ -15,6 +15,10 @@ elif get_option('buildtype') != 'debug'
endif
# test of i18n is in its own subdir
subdir('testi18n')
# scripts interpreted by extension-script-fu, installed to /scripts
scripts = [
'contactsheet.scm',

View file

@ -0,0 +1,2 @@
es
de

View file

@ -0,0 +1,17 @@
# A file for testing.
# For domain "scriptfu-test"
# Not actually from translators, hacked together by Lloyd Konneker
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
#: source line not identified, is used in test-i18n.scm
msgid "Orientation"
msgstr "Orientflugel"
msgid "Elevation"
msgstr "Eleveflugel"

View file

@ -0,0 +1,17 @@
# A file for testing.
# For domain "scriptfu-test"
# Not actually from translators, hacked together by Lloyd Konneker
msgid ""
msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
#: source line not identified, is used in test-i18n.scm
msgid "Orientation"
msgstr "Orientación"
msgid "Elevation"
msgstr "Elevación"

View file

@ -0,0 +1,44 @@
# test i18n plugins
# Not for translators: translation data is mocked up
# scripts interpreted by gimp-script-fu-interpreter
scripts_independent = [
{ 'name': 'test-i18n' },
{ 'name': 'test-i18n-more' },
]
foreach plugin : scripts_independent
name = plugin.get('name')
srcs = plugin.get('srcs', name + '.scm')
install_data(srcs,
install_dir: gimpplugindir / 'plug-ins' / name,
install_mode: 'rwxr-xr-x')
endforeach
# Install test translations for a suite of PDB procedures
# under two i18n domain names: "script-fu-test" and "scriptfu-test-more"
# See "Internationalizing" at the gimp developer website
# Two plugin files define three PDB procedures
# that share the same translations data.
# "Suite" means: sharing the same translations data files.
# suite is one-to-many with i18n domain names, unfortunately.
# ! install_dir: does NOT accept a list
# test-i18n.scm defines two PDB procedures
i18n.gettext ('scriptfu-test',
preset: 'glib',
install_dir: gimpplugindir / 'plug-ins' / 'test-i18n' / 'locale'
)
# test-i18n-more.scm defines one PDB procedure
# Duplicate, install i18n data in a second place
# and give it a different domain name, "scriptfu-test-more !!!
i18n.gettext ('scriptfu-test-more',
preset: 'glib',
install_dir: gimpplugindir / 'plug-ins' / 'test-i18n-more' / 'locale'
)

View file

@ -0,0 +1,41 @@
#!/usr/bin/env gimp-script-fu-interpreter-3.0
;!# Close comment started on first line. Needed by gettext.
; An independently interpreted Scheme plugin
; to test the registration function script-fu-register-18n.
; This is marked with translateable strings,
; but we don't expect translators to translate.
; We mock up incomplete translation data.
; Has translatable dialog.
; Dialog appears in native language
; when the script-fu-register-18n call is proper
; and mockup translation data installed corresponding to said call.
(define (plug-in-test-i18n-3 orientation)
; does nothing
)
; Not a filter, always enabled.
(script-fu-register-procedure "plug-in-test-i18n-3"
_"Test SF i18n Three..." ; menu item
"" ; tooltip
"LKK"
"2025"
; One arg, just to test the translation of its label
SF-OPTION _"Orientation" '(_"Horizontal"
_"Vertical")
)
(script-fu-menu-register "plug-in-test-i18n-3"
"<Image>/Filters/Development/Test")
; !!! Note the domain name is not the same as for the
; other two PDB procedures in the suite
(script-fu-register-i18n "plug-in-test-i18n-3" ; plugin name
"scriptfu-test-more") ; domain name

View file

@ -0,0 +1,202 @@
#!/usr/bin/env gimp-script-fu-interpreter-3.0
;!# Close comment started on first line. Needed by gettext.
; An independently interpreted Scheme plugin
; to test the registration function script-fu-register-18n.
; This is marked with translateable strings,
; but we don't expect translators to translate.
; We mock up translation data.
; Has translateable dialog.
; Dialog appears in native language
; only when the script-fu-register-18n call is proper
; and mockup translation data installed corresponding to said call.
(define (plug-in-test-i18n-1 orientation)
; does nothing
)
(define (plug-in-test-i18n-2 elevation)
; does nothing
)
; Not a filter, always enabled.
(script-fu-register-procedure "plug-in-test-i18n-1"
_"Test SF i18n One..." ; menu item
"" ; tooltip
"LKK"
"2025"
; a non-filter procedure has no image types, always enabled
; a non-filter procedure has no drawable arity, always enabled
; One arg, just to test the translation of its label
SF-OPTION _"Orientation" '(_"Horizontal"
_"Vertical")
)
(script-fu-register-procedure "plug-in-test-i18n-2"
_"Test SF i18n Two..." ; menu item
"" ; tooltip
"LKK"
"2025"
SF-OPTION _"Elevation" '(_"High"
_"Low")
)
(script-fu-menu-register "plug-in-test-i18n-1"
"<Image>/Filters/Development/Test")
(script-fu-menu-register "plug-in-test-i18n-2"
"<Image>/Filters/Development/Test")
; This documents the cases for a script to declare translations data.
; This is only for plugins installed to /plug-ins (independently interpreted.)
;
; The script must also have GUI strings marked for translation using notation _"foo".
; A script may be marked, but not have translator produced translation files.
;
; 1. The script can simply omit a call to script-fu-register-i18n.
; This means: there is no translation data for the procedure.
;
; 2. The script can call (script-fu-register-i18n "proc_name" "None")
; This also means: there is no translation data for the procedure.
;
; 3. The script can call (script-fu-register-i18n "proc_name" "Standard")
; The script will be in native language subject to the existence of translation files
; installed the usual, standard way.
; This means: the domain name is the script's name
; and the translation files are in .../plug-ins/<plugin_name>/locale directory
; for example for French language there exists a file
; .../plug-ins/<plugin_name>/locale/fr/LC_MESSAGES/<plugin_name>.mo
; all installed, for example,
; in GIMP's installed data e.g. /usr/lib/share/GIMP/3.0/plug-ins
; (when the plugin is an official plugin supported by GIMP)
; or in the user's ~/.config/GIMP/3.0/plug-ins
; (when the user wrote or installed a third-party plugin for their own private use.)
;
; 4. The script can call (script-fu-register-i18n "proc_name" "foo_domain" "bar_path")
; This means a custom install location for the translation files,
; and a custom name for the domain, i.e. name of the .mo files.
;
; Typically, this is NOT USEFUL: why change the names?
; Typically this is NOT USEFUL for a group of plugins sharing translation files.
;
; It is not useful for group of plugins to share translations data because
; "bar_path" must be a relative (not absolute) path to the plugin's directory,
; AND a subdirectory of the plugin's directory.
; NOT ALLOWED: "../bar" meaning parent dir of bar i.e. in /plug-ins.
; NOT ALLOWED: "/usr/bar" meaning some absolute path starting at root.
;
; As of this writing, the useful ways for a group of third-party plugins to share
; common translations data are:
; a. Put all the procedures in the same .scm file (you can do that)
; with a call (script-fu-register-i18n "proc_nameX" "Standard")
; for each procedure X in the .scm file,
; and install the usual way e.g. .../plug-ins/plugin-name/plugin-name.scm
; and install .../plug-ins/plugin-name/locale/fr/LC_MESSAGES/plugin-name.mo
; b. Install each plugin in its own subdirectory of .../plugins
; with a call (script-fu-register-i18n "proc_nameX" "Standard")
; and at install time, distribute the one shared translation file foo.mo
; to each of the many catalog directories, with renaming,
; e.g. to .../plug-ins/plugin-name1/locale/fr/LC_MESSAGES/plugin-name1.
; and also to .../plug-ins/plugin-name2/locale/fr/LC_MESSAGES/plugin-name2.
;
; In the example, the translations files will be found
; in .../plug-ins/proc_name/bar_path
; which will contain for example one or more files like
; .../plug-in/proc_name/bar_path/fr/LC_MESSAGES/foo_domain.mo
;
; 5. The script can call (script-fu-register-i18n "proc_name" "gimp30-script-fu")
; This means use the translation files
; common to all Scheme plugins distributed with GIMP.
; This is only useful for official plugins distributed with GIMP.
; It is not useful for third-party plugins,
; since they should not be installed in the sys GIMP data directory
; (else they will be lost in an upgrade)
; and also since the official translations data installed with GIMP
; should not be altered by a third-party plugin.
; Note that one script file may define many PDB procedures
; and call script-fu-register-i18n for each of them.
; Some procedures may be translated, and others not.
;
; A script can (but shouldn't) call script-fu-register-i18n more than once
; for the same procedure.
; Only the last one will have effect.
; These are test cases for calls to script-fu-register-i18n.
; To test, comment out one or more trailing cases and run again.
; Only the last uncommented one has effect.
; Valid use cases
; No errors at registration time, but can error at runtime
; depending on existence of translation data.
; Case: Standard translation data
; As of this writing there exists no mockup translation data for this case.
; Expect this will throw an error at plugin run time,
; to the console where GIMP was started,
; since the standard catalog directory (owned by the plugin) does not yet exist:
; .../plug-ins/test-i18n/locale/es/LC_MESSAGES/test-i18n.mo
;(script-fu-register-i18n "plug-in-test-i18n" "Standard")
; Case: no translation data, plugin not in native language.
; Just omit the call to script-fu-register-i18n
; Case: declare domain "None", no translation data, plugin not in native language.
; Expect this does not throw an error, and never translates the plugin.
; Same as previous case, but documents that no translation is done.
;(script-fu-register-i18n "plug-in-test-i18n" "None")
; Case: Rename the domain but not the catalog.
; Expect plugin translates when /plug-ins/test-i18n/locale exists
; and contains es/LC_MESSAGES/scriptfu-test.mo.
; Expect throws an error at plugin run time when said catalog directory not exist
; in a subdirectory of the plugin root dir
; i.e. /usr/local/lib/gimp/3.0/plug-ins/test-i18n/locale/es/LC_MESSAGES/scriptfu-test.mo
(script-fu-register-i18n "plug-in-test-i18n-1" ; plugin name
"scriptfu-test") ; domain name
(script-fu-register-i18n "plug-in-test-i18n-2" ; plugin name
"scriptfu-test") ; domain name
; This is the same as: (script-fu-register-i18n "plug-in-test-i18n" "fooDomain" "locale" )
; Case: Rename the domain and the catalog.
; Expect plugin translates when /plug-ins/plug-in-test-i18n/barCatalog exists
; and contains fr/LC_MESSAGES/fooDomain.mo.
; Expect throws an error at plugin run time when said catalog directory not exist.
; (script-fu-register-i18n "plug-in-test-i18n" "fooDomain" "barCatalog")
; Case: Rename the domain to the one shared by official GIMP ScriptFu plugins.
; Expect plugin translates when GIMP is properly installed,
; and the shared translations data contains translation pairs
; that match translateable strings in the plugin.
; Expect throws an error at plugin run time when GIMP is not properly installed.
; Expect strings in the plugin are in the native language
; (only since by design the string "Orientation" is in gimp30-script-fu.mo)
;(script-fu-register-i18n "plug-in-test-i18n" "gimp30-script-fu")
; Error cases:
; Error case: a relative path to a catalog above the plugin's directory.
; GIMP requires the catalog dir is a subdir of, i.e. beneath, a plugins' install dir.
; Expect this throws an error: "The catalog directory set by set_i18n() is not a subdirectory: ../bar"
; at plugin run time, not registration time.
;(script-fu-register-i18n "plug-in-test-i18n" "fooDomain" "../bar" )
; Error case: an absolute path to a catalog.
; Because GIMP requires path to the catalog dir is not absolute, i.e. starting with "/"
; Expect this throws an error: The catalog directory set by set_i18n() is not relative: /bar
; at plugin run time, not registration time.
;(script-fu-register-i18n "plug-in-test-i18n" "fooDomain" "/bar" )
; Error Case: not enough arguments.
; Expect: Error: script-fu-register-i18n takes two or three args
; in the console, at registration time, but the plugin will still work, without translations.
; (script-fu-register-i18n "plug-in-test-i18n")