Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3bda1d6
Configure Babel and Jinja to extract and render gettext-wrapped messa…
emilyanndavis Mar 11, 2026
d4a391f
Use current locale to set lang attr and populate UG URL in base templ…
emilyanndavis Mar 20, 2026
efcdf19
Reload model module before calling execute, to ensure text defined in…
emilyanndavis Mar 20, 2026
805a2f1
Wrap all report_constants-defined strings in functions to ensure they…
emilyanndavis Mar 20, 2026
2442ae9
Update base template w/ locale and trans/endtrans tags where needed.
emilyanndavis Mar 20, 2026
1e97509
Update partials and model-specific templates with gettext() or trans/…
emilyanndavis Mar 20, 2026
d816eb5
Update CV, SDR/NDR reporters with current locale.
emilyanndavis Mar 20, 2026
0caf99c
Update report template tests.
emilyanndavis Mar 23, 2026
ae40516
Misc report translation-related cleanup.
emilyanndavis Mar 23, 2026
24bd476
Add base template tests.
emilyanndavis Mar 23, 2026
b883a30
Update i18n readme with reports-related details.
emilyanndavis Mar 23, 2026
7cb66d5
Improve a checkbox label w/ context and singular/plural options.
emilyanndavis Mar 23, 2026
0364f7c
Adjust formatting of "Stream Network Maps" string to ensure correct m…
emilyanndavis Mar 23, 2026
70f19f7
Merge branch 'main' of github.com:natcap/invest into feature/2319-rep…
emilyanndavis Mar 23, 2026
ecf709f
Reload model module only if model has a reporter. Update CLI tests so…
emilyanndavis Mar 24, 2026
88ab86b
Remove commented-out code.
emilyanndavis Mar 25, 2026
95da27c
Remove errant comma.
emilyanndavis Mar 25, 2026
b7c0e5d
Streamline imports in `spec.py`.
emilyanndavis Mar 27, 2026
62e8ba2
Update i18n README with clarification about Workbench i18n.
emilyanndavis Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/natcap/invest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@
LOCALE_CODE = 'en'


def get_locale():
"""Get the current locale code."""
return LOCALE_CODE
Comment on lines +46 to +48
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This getter is needed so we can pass the current locale code to the base HTML template. If we import and reference LOCALE_CODE directly, it will always be 'en' because that was its initial value—it won't change even if it has been modified by set_locale.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should have done this originally, but may I suggest renaming the variable to _LOCALE_CODE to emphasize that it's not meant to be used outside this module?



def set_locale(locale_code):
"""Set the `gettext` attribute of natcap.invest.
Expand Down
11 changes: 6 additions & 5 deletions src/natcap/invest/carbon/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pygeoprocessing

from natcap.invest import __version__
from natcap.invest import gettext
from natcap.invest import gettext, get_locale
from natcap.invest.reports import jinja_env, raster_utils, report_constants
from natcap.invest.reports.raster_utils import RasterDatatype, RasterPlotConfig
from natcap.invest.spec import ModelSpec
Expand Down Expand Up @@ -131,7 +131,7 @@ def report(file_registry: dict, args_dict: dict, model_spec: ModelSpec,
raster_path=file_registry['c_storage_bas'],
datatype=RasterDatatype.continuous,
spec=model_spec.get_output('c_storage_bas'))]

intermediate_raster_config_lists = [[
RasterPlotConfig(
raster_path=file_registry[f'c_{pool_type}_bas'],
Expand Down Expand Up @@ -178,7 +178,7 @@ def report(file_registry: dict, args_dict: dict, model_spec: ModelSpec,
input_raster_config_list)
input_raster_caption = raster_utils.caption_raster_list(
input_raster_config_list)

outputs_img_src = raster_utils.plot_and_base64_encode_rasters(
output_raster_config_list)
output_raster_caption = raster_utils.caption_raster_list(
Expand Down Expand Up @@ -214,6 +214,7 @@ def report(file_registry: dict, args_dict: dict, model_spec: ModelSpec,

with open(target_html_filepath, 'w', encoding='utf-8') as target_file:
target_file.write(TEMPLATE.render(
locale=get_locale(),
report_script=model_spec.reporter,
invest_version=__version__,
report_filepath=target_html_filepath,
Expand All @@ -230,10 +231,10 @@ def report(file_registry: dict, args_dict: dict, model_spec: ModelSpec,
outputs_img_src=outputs_img_src,
outputs_caption=output_raster_caption,
intermediate_raster_sections=intermediate_raster_sections,
raster_group_caption=report_constants.RASTER_GROUP_CAPTION,
raster_group_caption=report_constants.raster_group_caption(),
output_raster_stats_table=output_raster_stats_table,
input_raster_stats_table=input_raster_stats_table,
stats_table_note=report_constants.STATS_TABLE_NOTE,
stats_table_note=report_constants.stats_table_note(),
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, it would be a bit more intuitive to keep the strings as constants and wrap them directly in gettext here:

Suggested change
stats_table_note=report_constants.stats_table_note(),
stats_table_note=gettext(report_constants.STATS_TABLE_NOTE),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree—it doesn't seem quite right to define "constants" as functions—but I think the reason I ended up doing that is because I wanted the constants module itself to handle the localization. While your suggestion makes sense (and works), it shifts the responsibility to each reporter module, and as more reports are added I worry that it would be too easy to forget to wrap those constants in gettext, and we don't currently have a mechanism to catch that mistake.

I was trying to avoid reloading the report_constants module, but since I'm not thrilled with these alternatives, I'm now thinking it might be best to follow the same pattern we use for localizing validation messages, and just go ahead and reload the report_constants module when we need it (in this case, that would be just before reloading the model module when we're about to run a model that has a reporter). Do you see any reason not to do that?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, well it is nice to avoid needing to reload the module. Reloading is really a kind of hacky antipattern, and I have wondered before if we should refactor other parts of invest to wrap translated objects in a function, rather than reloading. Since the reference to stats_table_note here is already wrapped in render(), it is evaluated in the correct language at runtime, so I think I prefer your current solution vs. introducing a need to reload.

I'd be curious what others think about the value of the wrapper functions as a "reminder" mechanism vs. directly calling gettext. If we keep the current design, I'd suggest renaming report_constants to report_messages or report_strings.

model_spec_outputs=model_spec.outputs,
))

Expand Down
17 changes: 10 additions & 7 deletions src/natcap/invest/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,14 @@
import json
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed some unused imports.

import logging
import multiprocessing
import os
import pprint
import sys
import textwrap
import warnings

import natcap.invest
from natcap.invest import datastack
from natcap.invest import set_locale
from natcap.invest import spec
from natcap.invest import ui_server
from natcap.invest import utils
from natcap.invest import models
from pygeoprocessing.utils import GDALUseExceptions

Expand Down Expand Up @@ -463,8 +459,15 @@ def main(user_args=None):

target_model = models.model_id_to_pyname[args.model]
model_module = importlib.import_module(name=target_model)
LOGGER.info('Imported target %s from %s',
model_module.__name__, model_module)

LOGGER.info(
f'Imported target {model_module.__name__} from {model_module}')

generate_report = bool(model_module.MODEL_SPEC.reporter)
if generate_report:
# Reload model module to ensure text defined in model spec is
# localized before it is passed to the report.
importlib.reload(model_module)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So in the past the model_module would call execute without localizing first, and that was okay because localization was not necessary for any reason during/after execute? But now it is necessary because the report uses localized strings from model_module?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The report uses strings from model_module—specifically, from model_module.MODEL_SPEC—and although those strings are wrapped in gettext, those calls to gettext are executed only when the model_module loads. When model_module first loads, the locale is en, so all those strings are in their default English. When the locale changes, we need a mechanism to localize those strings on demand, i.e., by forcing all those calls to gettext to run again. I noticed that, when cli.main is invoked with the validate command, we are reloading natcap.invest.validation_messages to ensure the validation messages are localized, so I figured an analogous approach would make sense here. Hypothetically, if we were to move model specs out to their own modules (something we've entertained at least once or twice), we could get away with reloading only the (hypothetical) model spec module. As long as the MODEL_SPEC is defined on the model_module, however, reloading model_module seems to be the way to go.

But also … your question got me wondering how we manage to localize model_spec-defined strings in the Workbench, and it looks like we are actually missing a piece of the puzzle, since I found some text that is appearing in English even though we have a Spanish translation for it. I'll be digging into that separate from this PR.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same thing happens in ui_server.py - we reload the model module before returning the spec or validation messages.


# We're deliberately not validating here because the user
# can just call ``invest validate <datastack>`` to validate.
Expand All @@ -479,7 +482,7 @@ def main(user_args=None):
generate_metadata=True,
save_file_registry=True,
check_outputs=False,
generate_report=bool(model_module.MODEL_SPEC.reporter))
generate_report=generate_report)

if args.subcommand == 'serve':
ui_server.app.run(port=args.port)
Expand Down
15 changes: 12 additions & 3 deletions src/natcap/invest/coastal_vulnerability/reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pandas

from natcap.invest import __version__
from natcap.invest import gettext
from natcap.invest import gettext, get_locale
import natcap.invest.spec
from natcap.invest.reports import jinja_env

Expand Down Expand Up @@ -222,9 +222,17 @@ def report(file_registry, args_dict, model_spec, target_html_filepath):

na_count = exposure_geo.exposure.isna().sum()
if na_count:
# There is some redundancy here to ensure the checkbox label is
# localized properly. Note that passing an f-string to gettext will not
# work; we must explicitly use .format() to populate variable values.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the issue that "point(s)" might not translate well into other languages?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep! Some languages use different adjective forms for singular vs. plural nouns; verbs often must change as well (that's true in English, but sometimes we can sidestep that with implied verbs, e.g., "1 point [is] missing" vs. "2 points [are] missing"); and changes in word order are not out of the question. It's typically best to provide the full context for both the singular option and the plural option.

(Shorthand forms that rely on the ability to perceive punctuation, like "point(s)" or "is/are", aren't great for screenreader users either, but that's a bit of an aside—the primary motivation here is support for translations.)

if na_count == 1:
null_cb_name = gettext(
'{na_count} point missing the exposure index. Show:')
else:
null_cb_name = gettext(
'{na_count} points missing the exposure index. Show:')
null_checkbox = altair.binding_checkbox(
name=(f"{na_count} "
f"{gettext('point(s) missing the exposure index. Show:')}"))
name=(null_cb_name.format(na_count=na_count)))
show_null = altair.param(value=False, bind=null_checkbox)
null_points_chart = base_points.add_params(
show_null
Expand Down Expand Up @@ -396,6 +404,7 @@ def report(file_registry, args_dict, model_spec, target_html_filepath):

with open(target_html_filepath, 'w', encoding='utf-8') as target_file:
target_file.write(TEMPLATE.render(
locale=get_locale(),
report_script=model_spec.reporter,
invest_version=__version__,
report_filepath=target_html_filepath,
Expand Down
12 changes: 9 additions & 3 deletions src/natcap/invest/internationalization/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ None of the translations files (.pot, .po, .mo) should be manually edited by us.
### `messages.pot`
Message catalog template file. This contains all the strings ("messages") that are translated, without any translations. All the PO files are derived from this.

### `babel_config.ini`
Mappings file that tells pybabel where to look when extracting messages into the message catalog. By default, pybabel will extract messages from Python source files; we need this mappings file to ensure it also extracts messages from the Jinja templates that are used for HTML reports.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this explanation!


### `locales/`
Locale directory. The contents of this directory are organized in a specific structure that `gettext` expects. `locales/` contains one subdirectory for each language for which there are any translations (not including the default English). The subdirectories are named after the corresponding ISO 639-1 language code. Each language subdirectory contains a directory `LC_MESSAGES`, which then contains the message catalog files for that language.

Expand All @@ -22,14 +25,15 @@ No changes are immediately needed when we add, remove, or edit strings that are
When we are ready to get a new batch of translations, here is the process. These instructions assume you have defined the two-letter locale code in an environment variable `$LL`.


1. Run the following from the root invest directory, replacing `<LANG>` with the language code:
1. Run the following from the root invest directory, replacing `$LL` with the language code:
```
pybabel extract \
--no-wrap \
--project InVEST \
--version $(python -m setuptools_scm) \
--msgid-bugs-address natcap-software@lists.stanford.edu \
--copyright-holder "Natural Capital Alliance" \
--mapping src/natcap/invest/internationalization/babel_config.ini \
--output src/natcap/invest/internationalization/messages.pot \
src/

Expand All @@ -39,7 +43,7 @@ pybabel update \
--input-file src/natcap/invest/internationalization/messages.pot \
--output-file src/natcap/invest/internationalization/locales/$LL/LC_MESSAGES/messages.po
```
This looks through the source code for strings wrapped in the `gettext(...)` function and writes them to the message catalog template. Then it updates the message catalog for the specificed language. New strings that don't yet have a translation will have an empty `msgstr` value. Previously translated messages that are no longer needed will be commented out but remain in the file. This will save translator time if they're needed again in the future.
This looks through the source code for strings wrapped in the `gettext(...)` function and writes them to the message catalog template. Then it updates the message catalog for the specified language. New strings that don't yet have a translation will have an empty `msgstr` value. Previously translated messages that are no longer needed will be commented out but remain in the file. This will save translator time if they're needed again in the future.

2. Check that the changes look correct, then commit:
```
Expand Down Expand Up @@ -69,7 +73,9 @@ Then follow the "Process to update translations" instructions above, starting fr
* Model titles
* `MODEL_SPEC` `name` and `about` text
* Validation messages
* Strings that appear in the UI, such as button labels and tooltip text
* Strings that appear in HTML reports, such as section headings, figure captions, and table column headers

Strings that appear exclusively in the Workbench UI, such as button labels and tooltip text, are also translated, but they are handled separately. See the [Workbench README](../../../../workbench/readme.md#internationalization) for details.

We are not translating:

Expand Down
9 changes: 9 additions & 0 deletions src/natcap/invest/internationalization/babel_config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Extract messages from Python source files
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I named this file babel_config.ini because the Babel docs on mapping files indicate "[t]he configuration file syntax is based on the format commonly found in .INI files on Windows systems […]." Since I didn't find any requirements specifying how this file should be named, I suspect we could call it whatever we want. Open to suggestions if anyone has a preference for some other name or file type.


[python: **.py]

# Extract messages from Jinja HTML templates

[jinja2: **/templates/**.html]
ignore_tags = script,style
include_attrs = alt
3 changes: 1 addition & 2 deletions src/natcap/invest/ndr/reporter.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from natcap.invest.reports import raster_utils
from natcap.invest.reports import report_constants
from natcap.invest.reports import sdr_ndr_report_generator
from natcap.invest.reports.raster_utils import RasterDatatype, RasterPlotConfig
Expand Down Expand Up @@ -110,7 +109,7 @@ def report(file_registry, args_dict, model_spec, target_html_filepath):
raster_path=file_registry['stream'],
datatype=RasterDatatype.binary_high_contrast,
spec=model_spec.get_output('stream'))
stream_config.caption += report_constants.STREAM_CAPTION_APPENDIX
stream_config.caption += report_constants.stream_caption_appendix()
intermediate_raster_plot_configs = [
masked_dem_config, what_drains_config, stream_config]

Expand Down
8 changes: 7 additions & 1 deletion src/natcap/invest/reports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@
import matplotlib
import pandas

from natcap.invest import gettext

jinja_env = jinja2.Environment(
loader=jinja2.PackageLoader('natcap.invest.reports', 'templates'),
autoescape=jinja2.select_autoescape(),
undefined=jinja2.StrictUndefined
undefined=jinja2.StrictUndefined,
extensions=['jinja2.ext.i18n'],
)

jinja_env.install_gettext_callables(
gettext=gettext, ngettext=None, newstyle=True)

MATPLOTLIB_PARAMS = {
'backend': 'agg',
# 'legend.fontsize': 'small',
Expand Down
48 changes: 31 additions & 17 deletions src/natcap/invest/reports/report_constants.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,35 @@
from natcap.invest import gettext

STATS_TABLE_NOTE = gettext(
'"Valid percent" indicates the percent of pixels that are not '
'nodata. Comparing "valid percent" values across rasters may help '
'you identify cases of unexpected nodata.'
)

RASTER_GROUP_CAPTION = gettext(
'If a plot title includes "resampled," that raster was resampled to '
'a lower resolution for rendering in this report. Full resolution '
'rasters are available in the output workspace.'
)

STREAM_CAPTION_APPENDIX = gettext(
' The stream network may look incomplete at this resolution, and '
'therefore it may be necessary to view the full-resolution raster '
'in GIS to assess its accuracy.'
)
# All `gettext`-wrapped strings defined in this module should be returned from
# a function, _not_ defined on the module object itself, since text defined at
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that the wrapper functions are adding anything here. It should be sufficient if we just make sure that natcap.invest.gettext is being called after the locale has been set.

# the module level is not localized unless/until the module is reloaded.
# Wrapping text strings in functions avoids the need to reload the module
# and ensures text is localized properly when it is needed.


def stats_table_note():
return gettext(
'"Valid percent" indicates the percent of pixels that are not '
'nodata. Comparing "valid percent" values across rasters may help '
'you identify cases of unexpected nodata.'
)


def raster_group_caption():
"""Get "pre-caption" note about raster resampling."""
return gettext(
'If a plot title includes "resampled," that raster was resampled to '
'a lower resolution for rendering in this report. Full resolution '
'rasters are available in the output workspace.'
)


def stream_caption_appendix():
return gettext(
' The stream network may look incomplete at this resolution, and '
'therefore it may be necessary to view the full-resolution raster '
'in GIS to assess its accuracy.'
)


TABLE_PAGINATION_THRESHOLD = 10
12 changes: 8 additions & 4 deletions src/natcap/invest/reports/sdr_ndr_report_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import time

from natcap.invest import __version__
from natcap.invest import gettext
from natcap.invest import gettext, get_locale
from natcap.invest.reports import (
jinja_env, report_constants, sdr_ndr_utils, raster_utils)

Expand Down Expand Up @@ -58,8 +58,11 @@ def report(file_registry, args_dict, model_spec, target_html_filepath,
intermediate_raster_plot_configs)
intermediates_caption = raster_utils.caption_raster_list(
intermediate_raster_plot_configs)
# Note that passing an f-string to gettext will not work;
# we must explicitly use .format() to populate variable values.
intermediate_outputs_heading = gettext(
f'Stream Network Maps (flow algorithm: {args_dict["flow_dir_algorithm"]})')
'Stream Network Maps (flow algorithm: {flow_dir_algorithm})'
).format(flow_dir_algorithm=args_dict["flow_dir_algorithm"].upper())

(ws_vector_table, ws_vector_totals_table) = (
sdr_ndr_utils.generate_results_table_from_vector(
Expand All @@ -81,6 +84,7 @@ def report(file_registry, args_dict, model_spec, target_html_filepath,

with open(target_html_filepath, 'w', encoding='utf-8') as target_file:
target_file.write(TEMPLATE.render(
locale=get_locale(),
report_script=model_spec.reporter,
invest_version=__version__,
report_filepath=target_html_filepath,
Expand All @@ -97,12 +101,12 @@ def report(file_registry, args_dict, model_spec, target_html_filepath,
intermediate_outputs_heading=intermediate_outputs_heading,
intermediate_outputs_img_src=intermediate_img_src,
intermediate_outputs_caption=intermediates_caption,
raster_group_caption=report_constants.RASTER_GROUP_CAPTION,
raster_group_caption=report_constants.raster_group_caption(),
ws_vector_table=ws_vector_table,
ws_vector_totals_table=ws_vector_totals_table,
output_raster_stats_table=output_raster_stats_table,
input_raster_stats_table=input_raster_stats_table,
stats_table_note=report_constants.STATS_TABLE_NOTE,
stats_table_note=report_constants.stats_table_note(),
model_spec_outputs=model_spec.outputs,
))

Expand Down
4 changes: 2 additions & 2 deletions src/natcap/invest/reports/templates/args-table.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Value{% endtrans %}</th>
</tr>
</thead>
<tbody>
Expand Down
10 changes: 7 additions & 3 deletions src/natcap/invest/reports/templates/base.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<html lang="{{ locale }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block page_title %}InVEST Results: {{ model_name }}{% endblock page_title %}</title>
<title>{% block page_title %}{% trans %}InVEST Results: {{ model_name }}{% endtrans %}{% endblock page_title %}</title>
{% block styles %}
{% include 'styles.html' %}
{% endblock styles %}
Expand All @@ -16,23 +16,27 @@
{% block content scoped %}
<h1>{{ self.page_title() }}</h1>
<p>{{ model_description }}</p>
{% trans %}
<p>To learn more about the {{ model_name }} model, visit the
<a
target="_blank"
href="https://storage.googleapis.com/releases.naturalcapitalproject.org/invest-userguide/latest/en/{{ userguide_page }}"
href="https://storage.googleapis.com/releases.naturalcapitalproject.org/invest-userguide/latest/{{ locale }}/{{ userguide_page }}"
>InVEST User Guide (opens in new browser window)</a>.
</p>
{% endtrans %}
{% endblock content %}
</main>
{% block footer %}
<footer class="report-container">
<div class="report-info">
{% trans %}
<p>
This report was generated by {{ report_script }}, InVEST version {{ invest_version }}.
</p>
<p>
It was saved to {{ report_filepath }} at {{ timestamp }}.
</p>
{% endtrans %}
</div>
</footer>
{% endblock footer %}
Expand Down
2 changes: 1 addition & 1 deletion src/natcap/invest/reports/templates/caption.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
{% endfor %}
{% if source_list %}
<p class="sources">
Sources: {{ source_list | join('; ') }}
{% trans %}Sources:{% endtrans %} {{ source_list | join('; ') }}
</p>
{% endif %}
{% endif %}
Expand Down
Loading
Loading