Exemplo n.º 1
0
class AnonymousCitationTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    def test_location(self):
        source = Mock(Source)
        self.assertIsInstance(AnonymousCitation(source).location, str)

    def test_replace(self):
        class _HasCitations(HasCitations, Entity):
            pass

        facts = [_HasCitations()]
        files = [File('F1', __file__)]
        source = Mock(Source)
        sut = AnonymousCitation(source)
        other = AnonymousCitation(source)
        other.facts = facts
        other.files = files
        sut.replace(other)
        self.assertEquals(facts, list(sut.facts))
        self.assertEquals(files, list(sut.files))
Exemplo n.º 2
0
Arquivo: site.py Projeto: patlx/betty
    async def __aenter__(self):
        if not self._site_stack:
            for plugin in self._plugins.values():
                await self._plugin_exit_stack.enter_async_context(plugin)

        self._default_translations = Translations(
            self.translations[self.locale])
        self._default_translations.install()

        if self._executor is None:
            self._executor = ExceptionRaisingExecutor(ProcessPoolExecutor())

        self._site_stack.append(self)

        return self
Exemplo n.º 3
0
    async def enter(self):
        if not self._app_stack:
            for extension in self._extensions.values():
                await self._extension_exit_stack.enter_async_context(extension)

        self._default_translations = Translations(
            self.translations[self.locale])
        self._default_translations.install()

        if self._executor is None:
            self._executor = ExceptionRaisingExecutor(ThreadPoolExecutor())

        self._app_stack.append(self)

        return self
Exemplo n.º 4
0
    async def with_locale(self, locale: str) -> App:
        """
        Temporarily change this application's locale and the global gettext translations.
        """
        locale = negotiate_locale(locale, [
            locale_configuration.locale
            for locale_configuration in self.configuration.locales
        ])

        if locale is None:
            raise ValueError('Locale "%s" is not enabled.' % locale)

        previous_locale = self._locale
        if locale == previous_locale:
            yield self
            return
        await self._wait_for_threads()

        self._locale = locale
        with Translations(self.translations[locale]):
            self.react.getattr('locale').react.trigger()
            yield self
            await self._wait_for_threads()

        self._locale = previous_locale
        self.react.getattr('locale').react.trigger()
Exemplo n.º 5
0
def render(site: Site) -> None:
    logger = logging.getLogger()
    site.resources.copytree(join('public', 'static'),
                            site.configuration.www_directory_path)
    static_environment = create_environment(site)
    render_tree(site.configuration.www_directory_path,
                static_environment, site.configuration)
    sass.render_tree(site.configuration.www_directory_path)
    for locale, locale_configuration in site.configuration.locales.items():
        localized_environment = create_environment(site, locale)
        if site.configuration.multilingual:
            www_directory_path = join(
                site.configuration.www_directory_path, locale_configuration.alias)
        else:
            www_directory_path = site.configuration.www_directory_path

        site.resources.copytree(
            join('public', 'localized'), www_directory_path)
        render_tree(www_directory_path,
                    localized_environment, site.configuration)

        _render_entity_type(www_directory_path, site.ancestry.files.values(
        ), 'file', site.configuration, locale, localized_environment)
        logger.info('Rendered %d files in %s.' %
                    (len(site.ancestry.files), locale))
        _render_entity_type(www_directory_path, site.ancestry.people.values(
        ), 'person', site.configuration, locale, localized_environment)
        logger.info('Rendered %d people in %s.' %
                    (len(site.ancestry.people), locale))
        _render_entity_type(www_directory_path, site.ancestry.places.values(
        ), 'place', site.configuration, locale, localized_environment)
        logger.info('Rendered %d places in %s.' %
                    (len(site.ancestry.places), locale))
        _render_entity_type(www_directory_path, site.ancestry.events.values(
        ), 'event', site.configuration, locale, localized_environment)
        logger.info('Rendered %d events in %s.' %
                    (len(site.ancestry.events), locale))
        _render_entity_type(www_directory_path, site.ancestry.citations.values(
        ), 'citation', site.configuration, locale, localized_environment)
        logger.info('Rendered %d citations in %s.' %
                    (len(site.ancestry.citations), locale))
        _render_entity_type(www_directory_path, site.ancestry.sources.values(
        ), 'source', site.configuration, locale, localized_environment)
        logger.info('Rendered %d sources in %s.' %
                    (len(site.ancestry.sources), locale))
        with Translations(site.translations[locale]):
            _render_openapi(www_directory_path, site)
        logger.info('Rendered OpenAPI documentation.')
    chmod(site.configuration.www_directory_path, 0o755)
    for directory_path, subdirectory_names, file_names in os.walk(site.configuration.www_directory_path):
        for subdirectory_name in subdirectory_names:
            chmod(join(directory_path, subdirectory_name), 0o755)
        for file_name in file_names:
            chmod(join(directory_path, file_name), 0o644)
    site.event_dispatcher.dispatch(PostRenderEvent())
Exemplo n.º 6
0
class AnonymizeSourceTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    def test_should_remove_citations(self) -> None:
        source = Source('S0', 'The Source')
        citation = Citation(None, source)
        source.citations.append(citation)
        anonymous_source = AnonymousSource()
        anonymize_source(source, anonymous_source)
        self.assertEquals(0, len(source.citations))
        self.assertIn(citation, anonymous_source.citations)

    def test_should_remove_contained_by(self) -> None:
        source = Source('S0', 'The Source')
        contained_by = Source(None, 'The Source')
        source.contained_by = contained_by
        anonymous_source = AnonymousSource()
        anonymize_source(source, anonymous_source)
        self.assertIsNone(source.contained_by)

    def test_should_remove_contains(self) -> None:
        source = Source('S0', 'The Source')
        contains = Source(None, 'The Source')
        source.contains.append(contains)
        anonymous_source = AnonymousSource()
        anonymize_source(source, anonymous_source)
        self.assertEquals(0, len(source.contains))
        self.assertIn(contains, anonymous_source.contains)

    def test_should_remove_files(self) -> None:
        source = Source('S0', 'The Source')
        file = File('F0', __file__)
        source.files.append(file)
        anonymous_source = AnonymousSource()
        anonymize_source(source, anonymous_source)
        self.assertEquals(0, len(source.files))
        self.assertIn(file, anonymous_source.files)
Exemplo n.º 7
0
class AnonymizeCitationTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    def test_should_remove_facts(self) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        fact = PersonName(Person(None), 'Jane')
        citation.facts.append(fact)
        anonymous_source = AnonymousSource()
        anonymous_citation = AnonymousCitation(anonymous_source)
        anonymize_citation(citation, anonymous_citation)
        self.assertEquals(0, len(citation.facts))
        self.assertIn(fact, anonymous_citation.facts)

    def test_should_remove_files(self) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        file = File('F0', __file__)
        citation.files.append(file)
        anonymous_source = AnonymousSource()
        anonymous_citation = AnonymousCitation(anonymous_source)
        anonymize_citation(citation, anonymous_citation)
        self.assertEquals(0, len(citation.files))
        self.assertIn(file, anonymous_citation.files)

    def test_should_remove_source(self) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        anonymous_source = AnonymousSource()
        anonymous_citation = AnonymousCitation(anonymous_source)
        anonymize_citation(citation, anonymous_citation)
        self.assertIsNone(citation.source)
Exemplo n.º 8
0
class AnonymousSourceTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    def test_name(self):
        self.assertIsInstance(AnonymousSource().name, str)

    def test_replace(self):
        citations = [Citation(None, Source(None))]
        contains = [Source(None)]
        files = [Mock(File)]
        sut = AnonymousSource()
        other = AnonymousSource()
        other.citations = citations
        other.contains = contains
        other.files = files
        sut.replace(other)
        self.assertEquals(citations, list(sut.citations))
        self.assertEquals(contains, list(sut.contains))
        self.assertEquals(files, list(sut.files))
Exemplo n.º 9
0
 def test_label(self) -> None:
     with Translations(NullTranslations()):
         sut = UnknownEventType()
         self.assertIsInstance(sut.label, str)
         self.assertNotEqual('', sut.label)
Exemplo n.º 10
0
 def test_label(self) -> None:
     sut = Attendee()
     with Translations(NullTranslations()):
         self.assertIsInstance(sut.label, str)
         self.assertNotEqual('', sut.label)
Exemplo n.º 11
0
 def test(self, expected: str, datey: Datey):
     locale = 'en'
     with Translations(gettext.NullTranslations()):
         self.assertEquals(expected, format_datey(datey, locale))
Exemplo n.º 12
0
Arquivo: site.py Projeto: patlx/betty
class Site:
    def __init__(self, configuration: Configuration):
        self._site_stack = []
        self._ancestry = Ancestry()
        self._configuration = configuration
        self._assets = FileSystem(join(dirname(abspath(__file__)), 'assets'))
        self._dispatcher = Dispatcher()
        self._localized_url_generator = SiteUrlGenerator(configuration)
        self._static_url_generator = StaticPathUrlGenerator(configuration)
        self._locale = None
        self._translations = defaultdict(gettext.NullTranslations)
        self._default_translations = None
        self._plugins = OrderedDict()
        self._plugin_exit_stack = AsyncExitStack()
        self._init_plugins()
        self._init_dispatch_handlers()
        self._init_assets()
        self._init_translations()
        self._jinja2_environment = None
        self._renderer = None
        self._executor = None
        self._locks = Locks()

    async def __aenter__(self):
        if not self._site_stack:
            for plugin in self._plugins.values():
                await self._plugin_exit_stack.enter_async_context(plugin)

        self._default_translations = Translations(
            self.translations[self.locale])
        self._default_translations.install()

        if self._executor is None:
            self._executor = ExceptionRaisingExecutor(ProcessPoolExecutor())

        self._site_stack.append(self)

        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        self._site_stack.pop()

        self._default_translations.uninstall()

        if not self._site_stack:
            self._executor.shutdown()
            self._executor = None
            await self._plugin_exit_stack.aclose()

    @property
    def locale(self) -> str:
        if self._locale is not None:
            return self._locale
        return self._configuration.default_locale

    def _init_plugins(self) -> None:
        from betty.plugin import NO_CONFIGURATION

        def _extend_plugin_type_graph(graph: Graph,
                                      plugin_type: Type['Plugin']):
            dependencies = plugin_type.depends_on()
            # Ensure each plugin type appears in the graph, even if they're isolated.
            graph.setdefault(plugin_type, set())
            for dependency in dependencies:
                seen_dependency = dependency in graph
                graph[dependency].add(plugin_type)
                if not seen_dependency:
                    _extend_plugin_type_graph(graph, dependency)

        plugin_types_graph = defaultdict(set)
        # Add dependencies to the plugin graph.
        for plugin_type, _ in self._configuration.plugins:
            _extend_plugin_type_graph(plugin_types_graph, plugin_type)
        # Now all dependencies have been collected, extend the graph with optional plugin orders.
        for plugin_type, _ in self._configuration.plugins:
            for before in plugin_type.comes_before():
                if before in plugin_types_graph:
                    plugin_types_graph[plugin_type].add(before)
            for after in plugin_type.comes_after():
                if after in plugin_types_graph:
                    plugin_types_graph[after].add(plugin_type)

        for plugin_type in tsort(plugin_types_graph):
            plugin_configuration = self.configuration.plugins[
                plugin_type] if plugin_type in self.configuration.plugins else NO_CONFIGURATION
            plugin = plugin_type.for_site(self, plugin_configuration)
            self._plugins[plugin_type] = plugin

    def _init_dispatch_handlers(self) -> None:
        for plugin in self._plugins.values():
            self._dispatcher.append_handler(plugin)

    def _init_assets(self) -> None:
        for plugin in self._plugins.values():
            if plugin.assets_directory_path is not None:
                self._assets.paths.appendleft(plugin.assets_directory_path)
        if self._configuration.assets_directory_path:
            self._assets.paths.appendleft(
                self._configuration.assets_directory_path)

    def _init_translations(self) -> None:
        self._translations['en-US'] = gettext.NullTranslations()
        for locale in self._configuration.locales:
            for assets_path in reversed(self._assets.paths):
                translations = open_translations(locale, assets_path)
                if translations:
                    translations.add_fallback(self._translations[locale])
                    self._translations[locale] = translations

    @property
    def ancestry(self) -> Ancestry:
        return self._ancestry

    @property
    def configuration(self) -> Configuration:
        return self._configuration

    @property
    def plugins(self) -> Dict[Type['Plugin'], 'Plugin']:
        return self._plugins

    @property
    def assets(self) -> FileSystem:
        return self._assets

    @property
    def dispatcher(self) -> Dispatcher:
        return self._dispatcher

    @property
    def localized_url_generator(self) -> LocalizedUrlGenerator:
        return self._localized_url_generator

    @property
    def static_url_generator(self) -> StaticUrlGenerator:
        return self._static_url_generator

    @property
    def translations(self) -> Dict[str, gettext.NullTranslations]:
        return self._translations

    @property
    def jinja2_environment(self) -> Environment:
        if not self._jinja2_environment:
            from betty.jinja2 import BettyEnvironment
            self._jinja2_environment = BettyEnvironment(self)

        return self._jinja2_environment

    @property
    def renderer(self) -> Renderer:
        if not self._renderer:
            from betty.jinja2 import Jinja2Renderer
            self._renderer = SequentialRenderer([
                Jinja2Renderer(self.jinja2_environment, self._configuration),
                SassRenderer(),
            ])

        return self._renderer

    @property
    def executor(self) -> Executor:
        if self._executor is None:
            raise RuntimeError(
                "Cannot get the executor before this site's context is entered."
            )
        return self._executor

    @property
    def locks(self) -> Locks:
        return self._locks

    def with_locale(self, locale: str) -> 'Site':
        locale = negotiate_locale(locale,
                                  list(self.configuration.locales.keys()))
        if locale is None:
            raise ValueError('Locale "%s" is not enabled.' % locale)
        if locale == self.locale:
            return self

        site = copy(self)
        site._locale = locale

        # Clear all locale-dependent lazy-loaded attributes.
        site._jinja2_environment = None
        site._renderer = None

        return site
Exemplo n.º 13
0
class App:
    def __init__(self, configuration: Configuration):
        self._app_stack = []
        self._ancestry = Ancestry()
        self._configuration = configuration
        self._assets = FileSystem(join(dirname(abspath(__file__)), 'assets'))
        self._dispatcher = None
        self._localized_url_generator = AppUrlGenerator(configuration)
        self._static_url_generator = StaticPathUrlGenerator(configuration)
        self._locale = None
        self._translations = defaultdict(gettext.NullTranslations)
        self._default_translations = None
        self._extensions = OrderedDict()
        self._extension_exit_stack = AsyncExitStack()
        self._init_extensions()
        self._init_dispatcher()
        self._init_assets()
        self._init_translations()
        self._jinja2_environment = None
        self._renderer = None
        self._executor = None
        self._locks = Locks()

    async def enter(self):
        if not self._app_stack:
            for extension in self._extensions.values():
                await self._extension_exit_stack.enter_async_context(extension)

        self._default_translations = Translations(
            self.translations[self.locale])
        self._default_translations.install()

        if self._executor is None:
            self._executor = ExceptionRaisingExecutor(ThreadPoolExecutor())

        self._app_stack.append(self)

        return self

    async def exit(self):
        self._app_stack.pop()

        self._default_translations.uninstall()

        if not self._app_stack:
            self._executor.shutdown()
            self._executor = None
            await self._extension_exit_stack.aclose()

    async def __aenter__(self) -> 'App':
        return await self.enter()

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.exit()

    @property
    def locale(self) -> str:
        if self._locale is not None:
            return self._locale
        return self._configuration.default_locale

    def _init_extensions(self) -> None:
        for grouped_extension_types in tsort_grouped(
                build_extension_type_graph(
                    set(self._configuration.extensions.keys()))):
            for extension_type in grouped_extension_types:
                extension_args = []
                if issubclass(
                        extension_type, ConfigurableExtension
                ) and extension_type in self.configuration.extensions:
                    extension_kwargs = self.configuration.extensions[
                        extension_type]
                else:
                    extension_kwargs = {}

                if issubclass(extension_type, AppAwareFactory):
                    extension = extension_type.new_for_app(
                        self, *extension_args, **extension_kwargs)
                else:
                    extension = extension_type(*extension_args,
                                               **extension_kwargs)

                self._extensions[extension_type] = extension

    def _init_dispatcher(self) -> None:
        from betty.extension import ExtensionDispatcher

        self._dispatcher = ExtensionDispatcher(self._extensions.values())

    def _init_assets(self) -> None:
        for extension in self._extensions.values():
            if extension.assets_directory_path is not None:
                self._assets.paths.appendleft(extension.assets_directory_path)
        if self._configuration.assets_directory_path:
            self._assets.paths.appendleft(
                self._configuration.assets_directory_path)

    def _init_translations(self) -> None:
        self._translations['en-US'] = gettext.NullTranslations()
        for locale in self._configuration.locales:
            for assets_path in reversed(self._assets.paths):
                translations = open_translations(locale, assets_path)
                if translations:
                    translations.add_fallback(self._translations[locale])
                    self._translations[locale] = translations

    @property
    def ancestry(self) -> Ancestry:
        return self._ancestry

    @property
    def configuration(self) -> Configuration:
        return self._configuration

    @property
    def extensions(self) -> Dict[Type[Extension], Extension]:
        return self._extensions

    @property
    def assets(self) -> FileSystem:
        return self._assets

    @property
    def dispatcher(self) -> Dispatcher:
        return self._dispatcher

    @property
    def localized_url_generator(self) -> LocalizedUrlGenerator:
        return self._localized_url_generator

    @property
    def static_url_generator(self) -> StaticUrlGenerator:
        return self._static_url_generator

    @property
    def translations(self) -> Dict[str, gettext.NullTranslations]:
        return self._translations

    @property
    def jinja2_environment(self) -> Environment:
        if not self._jinja2_environment:
            from betty.jinja2 import BettyEnvironment
            self._jinja2_environment = BettyEnvironment(self)

        return self._jinja2_environment

    @property
    def renderer(self) -> Renderer:
        if not self._renderer:
            from betty.jinja2 import Jinja2Renderer
            self._renderer = SequentialRenderer([
                Jinja2Renderer(self.jinja2_environment, self._configuration),
            ])

        return self._renderer

    @property
    def executor(self) -> Executor:
        if self._executor is None:
            raise RuntimeError(
                "Cannot get the executor before this app's context is entered."
            )
        return self._executor

    @property
    def locks(self) -> Locks:
        return self._locks

    def with_locale(self, locale: str) -> 'App':
        locale = negotiate_locale(locale,
                                  list(self.configuration.locales.keys()))
        if locale is None:
            raise ValueError('Locale "%s" is not enabled.' % locale)
        if locale == self.locale:
            return self

        app = copy(self)
        app._locale = locale

        # Clear all locale-dependent lazy-loaded attributes.
        app._jinja2_environment = None
        app._renderer = None

        return app
Exemplo n.º 14
0
 def setUp(self) -> None:
     self._translations = Translations(gettext.NullTranslations())
     self._translations.install()
Exemplo n.º 15
0
class AnonymizeTest(TestCase):
    def setUp(self) -> None:
        self._translations = Translations(gettext.NullTranslations())
        self._translations.install()

    def tearDown(self) -> None:
        self._translations.uninstall()

    @patch('betty.extension.anonymizer.anonymize_person')
    def test_with_public_person_should_not_anonymize(
            self, m_anonymize_person) -> None:
        person = Person('P0')
        person.private = False
        ancestry = Ancestry()
        ancestry.entities.append(person)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_person.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_person')
    def test_with_private_person_should_anonymize(self,
                                                  m_anonymize_person) -> None:
        person = Person('P0')
        person.private = True
        ancestry = Ancestry()
        ancestry.entities.append(person)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_person.assert_called_once_with(person)

    @patch('betty.extension.anonymizer.anonymize_event')
    def test_with_public_event_should_not_anonymize(self,
                                                    m_anonymize_event) -> None:
        event = Event('E0', Birth())
        event.private = False
        ancestry = Ancestry()
        ancestry.entities.append(event)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_event.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_event')
    def test_with_private_event_should_anonymize(self,
                                                 m_anonymize_event) -> None:
        event = Event('E0', Birth())
        event.private = True
        ancestry = Ancestry()
        ancestry.entities.append(event)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_event.assert_called_once_with(event)

    @patch('betty.extension.anonymizer.anonymize_file')
    def test_with_public_file_should_not_anonymize(self,
                                                   m_anonymize_file) -> None:
        file = File('F0', __file__)
        file.private = False
        ancestry = Ancestry()
        ancestry.entities.append(file)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_file.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_file')
    def test_with_private_file_should_anonymize(self,
                                                m_anonymize_file) -> None:
        file = File('F0', __file__)
        file.private = True
        ancestry = Ancestry()
        ancestry.entities.append(file)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_file.assert_called_once_with(file)

    @patch('betty.extension.anonymizer.anonymize_source')
    def test_with_public_source_should_not_anonymize(
            self, m_anonymize_source) -> None:
        source = Source('S0', 'The Source')
        source.private = False
        ancestry = Ancestry()
        ancestry.entities.append(source)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_source.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_source')
    def test_with_private_source_should_anonymize(self,
                                                  m_anonymize_source) -> None:
        source = Source('S0', 'The Source')
        source.private = True
        ancestry = Ancestry()
        ancestry.entities.append(source)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_source.assert_called_once_with(source, ANY)

    @patch('betty.extension.anonymizer.anonymize_citation')
    def test_with_public_citation_should_not_anonymize(
            self, m_anonymize_citation) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        citation.private = False
        ancestry = Ancestry()
        ancestry.entities.append(citation)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_citation.assert_not_called()

    @patch('betty.extension.anonymizer.anonymize_citation')
    def test_with_private_citation_should_anonymize(
            self, m_anonymize_citation) -> None:
        source = Source('The Source')
        citation = Citation('C0', source)
        citation.private = True
        ancestry = Ancestry()
        ancestry.entities.append(citation)
        anonymize(ancestry, AnonymousCitation(AnonymousSource()))
        m_anonymize_citation.assert_called_once_with(citation, ANY)
Exemplo n.º 16
0
 def test_label(self) -> None:
     with Translations(NullTranslations()):
         sut = DivorceAnnouncement()
         self.assertIsInstance(sut.label, str)
         self.assertNotEqual('', sut.label)
Exemplo n.º 17
0
 def test_label(self) -> None:
     with Translations(NullTranslations()):
         sut = Missing()
         self.assertIsInstance(sut.label, str)
         self.assertNotEqual('', sut.label)
Exemplo n.º 18
0
 def _filter_format_date(date: Datey):
     with Translations(site.translations[default_locale]):
         return format_datey(date, default_locale)