2017-03-05 15:47:28 +01:00
/**************************************************************************/
/* resource_importer_csv_translation.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
2018-01-05 00:50:27 +01:00
2017-02-01 20:41:05 -03:00
# include "resource_importer_csv_translation.h"
2017-03-05 15:47:28 +01:00
2021-06-11 14:51:48 +02:00
# include "core/io/file_access.h"
2018-09-11 18:13:45 +02:00
# include "core/io/resource_saver.h"
2021-01-16 15:28:54 +00:00
# include "core/string/optimized_translation.h"
2024-08-15 15:00:47 +08:00
# include "core/string/translation_server.h"
2017-02-01 20:41:05 -03:00
String ResourceImporterCSVTranslation : : get_importer_name ( ) const {
return " csv_translation " ;
}
String ResourceImporterCSVTranslation : : get_visible_name ( ) const {
return " CSV Translation " ;
}
2020-05-14 14:29:06 +02:00
2017-02-01 20:41:05 -03:00
void ResourceImporterCSVTranslation : : get_recognized_extensions ( List < String > * p_extensions ) const {
p_extensions - > push_back ( " csv " ) ;
}
String ResourceImporterCSVTranslation : : get_save_extension ( ) const {
2018-02-21 11:30:55 -05:00
return " " ; //does not save a single resource
2017-02-01 20:41:05 -03:00
}
String ResourceImporterCSVTranslation : : get_resource_type ( ) const {
2017-05-20 22:49:34 +02:00
return " Translation " ;
2017-02-01 20:41:05 -03:00
}
2022-05-13 15:04:37 +02:00
bool ResourceImporterCSVTranslation : : get_option_visibility ( const String & p_path , const String & p_option , const HashMap < StringName , Variant > & p_options ) const {
2017-02-01 20:41:05 -03:00
return true ;
}
int ResourceImporterCSVTranslation : : get_preset_count ( ) const {
return 0 ;
}
2020-05-14 14:29:06 +02:00
2017-02-01 20:41:05 -03:00
String ResourceImporterCSVTranslation : : get_preset_name ( int p_idx ) const {
return " " ;
}
2021-11-14 14:02:38 -03:00
void ResourceImporterCSVTranslation : : get_import_options ( const String & p_path , List < ImportOption > * r_options , int p_preset ) const {
2025-10-24 12:00:03 +08:00
r_options - > push_back ( ImportOption ( PropertyInfo ( Variant : : INT , " compress " , PROPERTY_HINT_ENUM , " Disabled,Auto " ) , 1 ) ) ; // Enum for compatibility with previous versions.
2017-08-27 22:14:09 +07:00
r_options - > push_back ( ImportOption ( PropertyInfo ( Variant : : INT , " delimiter " , PROPERTY_HINT_ENUM , " Comma,Semicolon,Tab " ) , 0 ) ) ;
2025-10-24 12:00:03 +08:00
r_options - > push_back ( ImportOption ( PropertyInfo ( Variant : : BOOL , " unescape_keys " ) , false ) ) ;
r_options - > push_back ( ImportOption ( PropertyInfo ( Variant : : BOOL , " unescape_translations " ) , true ) ) ;
2017-02-01 20:41:05 -03:00
}
2024-09-23 16:07:40 +02:00
Error ResourceImporterCSVTranslation : : import ( ResourceUID : : ID p_source_id , const String & p_source_file , const String & p_save_path , const HashMap < StringName , Variant > & p_options , List < String > * r_platform_variants , List < String > * r_gen_files , Variant * r_metadata ) {
2025-10-24 12:00:03 +08:00
Ref < FileAccess > f = FileAccess : : open ( p_source_file , FileAccess : : READ ) ;
ERR_FAIL_COND_V_MSG ( f . is_null ( ) , ERR_INVALID_PARAMETER , " Cannot open file from path ' " + p_source_file + " '. " ) ;
2017-08-27 22:14:09 +07:00
String delimiter ;
switch ( ( int ) p_options [ " delimiter " ] ) {
2025-10-24 12:00:03 +08:00
case 1 : {
2020-05-10 13:00:47 +02:00
delimiter = " ; " ;
2025-10-24 12:00:03 +08:00
} break ;
case 2 : {
2020-05-10 13:00:47 +02:00
delimiter = " \t " ;
2025-10-24 12:00:03 +08:00
} break ;
default : {
delimiter = " , " ;
} break ;
2017-08-27 22:14:09 +07:00
}
2025-10-24 12:00:03 +08:00
// Parse the header row.
HashMap < int , Ref < Translation > > column_to_translation ;
int context_column = - 1 ;
int plural_column = - 1 ;
{
const Vector < String > line = f - > get_csv_line ( delimiter ) ;
for ( int i = 1 ; i < line . size ( ) ; i + + ) {
if ( line [ i ] . left ( 1 ) = = " _ " ) {
continue ;
}
if ( line [ i ] . to_lower ( ) = = " ?context " ) {
ERR_CONTINUE_MSG ( context_column ! = - 1 , " Error importing CSV translation: Multiple '?context' columns found. Only one is allowed. Subsequent ones will be ignored. " ) ;
context_column = i ;
continue ;
}
if ( line [ i ] . to_lower ( ) = = " ?plural " ) {
ERR_CONTINUE_MSG ( plural_column ! = - 1 , " Error importing CSV translation: Multiple '?plural' columns found. Only one is allowed. Subsequent ones will be ignored. " ) ;
plural_column = i ;
continue ;
}
2017-02-01 20:41:05 -03:00
2025-10-24 12:00:03 +08:00
const String locale = TranslationServer : : get_singleton ( ) - > standardize_locale ( line [ i ] ) ;
ERR_CONTINUE_MSG ( locale . is_empty ( ) , vformat ( " Error importing CSV translation: Invalid locale format '%s', should be 'language_Script_COUNTRY_VARIANT@extra'. This column will be ignored. " , line [ i ] ) ) ;
2017-02-01 20:41:05 -03:00
2025-10-24 12:00:03 +08:00
Ref < Translation > translation ;
translation . instantiate ( ) ;
translation - > set_locale ( locale ) ;
column_to_translation [ i ] = translation ;
2023-10-19 12:55:35 +08:00
}
2025-10-29 17:44:13 +08:00
if ( column_to_translation . is_empty ( ) ) {
WARN_PRINT ( vformat ( " CSV file '%s' does not contain any translation. " , p_source_file ) ) ;
return OK ;
}
2017-02-01 20:41:05 -03:00
}
2025-10-24 12:00:03 +08:00
// Parse content rows.
bool context_used = false ;
bool plural_used = false ;
{
const bool unescape_keys = p_options . has ( " unescape_keys " ) ? bool ( p_options [ " unescape_keys " ] ) : false ;
const bool unescape_translations = p_options . has ( " unescape_translations " ) ? bool ( p_options [ " unescape_translations " ] ) : true ;
bool reading_plural_rows = false ;
String plural_msgid ;
String plural_msgctxt ;
HashMap < int , Vector < String > > plural_msgstrs ;
do {
const Vector < String > line = f - > get_csv_line ( delimiter ) ;
// Skip empty lines.
if ( line . size ( ) = = 1 & & line [ 0 ] . is_empty ( ) ) {
continue ;
}
if ( line [ 0 ] . to_lower ( ) = = " ?pluralrule " ) {
for ( int i = 1 ; i < line . size ( ) ; i + + ) {
if ( line [ i ] . is_empty ( ) | | ! column_to_translation . has ( i ) ) {
continue ;
}
Ref < Translation > translation = column_to_translation [ i ] ;
ERR_CONTINUE_MSG ( ! translation - > get_plural_rules_override ( ) . is_empty ( ) , vformat ( " Error importing CSV translation: Multiple '?pluralrule' definitions found for locale '%s'. Only one is allowed. Subsequent ones will be ignored. " , translation - > get_locale ( ) ) ) ;
translation - > set_plural_rules_override ( line [ i ] ) ;
}
continue ;
}
const String msgid = unescape_keys ? line [ 0 ] . c_unescape ( ) : line [ 0 ] ;
if ( ! reading_plural_rows & & msgid . is_empty ( ) ) {
continue ;
}
// It's okay if you define context or plural columns but don't use them.
const String msgctxt = ( context_column ! = - 1 & & context_column < line . size ( ) ) ? line [ context_column ] : String ( ) ;
if ( ! msgctxt . is_empty ( ) ) {
context_used = true ;
}
const String msgid_plural = ( plural_column ! = - 1 & & plural_column < line . size ( ) ) ? line [ plural_column ] : String ( ) ;
if ( ! msgid_plural . is_empty ( ) ) {
plural_used = true ;
}
// End of plural rows.
if ( reading_plural_rows & & ( ! msgid . is_empty ( ) | | ! msgctxt . is_empty ( ) | | ! msgid_plural . is_empty ( ) ) ) {
reading_plural_rows = false ;
for ( KeyValue < int , Ref < Translation > > E : column_to_translation ) {
Ref < Translation > translation = E . value ;
const Vector < String > & msgstrs = plural_msgstrs [ E . key ] ;
if ( ! msgstrs . is_empty ( ) ) {
translation - > add_plural_message ( plural_msgid , msgstrs , plural_msgctxt ) ;
}
}
plural_msgstrs . clear ( ) ;
}
// Start of plural rows.
if ( ! reading_plural_rows & & ! msgid_plural . is_empty ( ) ) {
reading_plural_rows = true ;
plural_msgid = msgid ;
plural_msgctxt = msgctxt ;
}
2021-03-19 19:50:53 +01:00
2017-02-01 20:41:05 -03:00
for ( int i = 1 ; i < line . size ( ) ; i + + ) {
2025-10-24 12:00:03 +08:00
if ( ! column_to_translation . has ( i ) ) {
2023-10-19 12:55:35 +08:00
continue ;
}
2025-10-24 12:00:03 +08:00
const String msgstr = unescape_translations ? line [ i ] . c_unescape ( ) : line [ i ] ;
if ( msgstr . is_empty ( ) ) {
continue ;
}
if ( reading_plural_rows ) {
plural_msgstrs [ i ] . push_back ( msgstr ) ;
} else {
column_to_translation [ i ] - > add_message ( msgid , msgstr , msgctxt ) ;
}
}
} while ( ! f - > eof_reached ( ) ) ;
if ( reading_plural_rows ) {
for ( KeyValue < int , Ref < Translation > > E : column_to_translation ) {
Ref < Translation > translation = E . value ;
const Vector < String > & msgstrs = plural_msgstrs [ E . key ] ;
if ( ! msgstrs . is_empty ( ) ) {
translation - > add_plural_message ( plural_msgid , msgstrs , plural_msgctxt ) ;
}
2017-02-01 20:41:05 -03:00
}
}
2025-10-24 12:00:03 +08:00
}
bool compress ;
switch ( ( int ) p_options [ " compress " ] ) {
case 0 : { // Disabled.
compress = false ;
} break ;
default : { // Auto.
compress = ! context_used & & ! plural_used ;
} break ;
}
2017-02-01 20:41:05 -03:00
2025-10-24 12:00:03 +08:00
for ( KeyValue < int , Ref < Translation > > E : column_to_translation ) {
Ref < Translation > xlt = E . value ;
2017-02-01 20:41:05 -03:00
if ( compress ) {
2021-01-16 15:28:54 +00:00
Ref < OptimizedTranslation > cxl = memnew ( OptimizedTranslation ) ;
2017-02-01 20:41:05 -03:00
cxl - > generate ( xlt ) ;
xlt = cxl ;
}
2025-10-24 12:00:03 +08:00
String save_path = p_source_file . get_basename ( ) + " . " + xlt - > get_locale ( ) + " .translation " ;
ResourceUID : : ID save_id = hash64_murmur3_64 ( xlt - > get_locale ( ) . hash64 ( ) , p_source_id ) & 0x7FFFFFFFFFFFFFFF ;
2025-02-21 01:19:40 -08:00
bool uid_already_exists = ResourceUID : : get_singleton ( ) - > has_id ( save_id ) ;
if ( uid_already_exists ) {
// Avoid creating a new file with a duplicate UID.
// Always use this UID, even if the user has moved it to a different path.
save_path = ResourceUID : : get_singleton ( ) - > get_id_path ( save_id ) ;
}
2017-02-01 20:41:05 -03:00
2022-06-03 01:33:42 +02:00
ResourceSaver : : save ( xlt , save_path ) ;
2017-02-01 20:41:05 -03:00
if ( r_gen_files ) {
r_gen_files - > push_back ( save_path ) ;
}
2025-02-21 01:19:40 -08:00
if ( ! uid_already_exists ) {
// No need to call set_uid if save_path already refers to save_id.
ResourceSaver : : set_uid ( save_path , save_id ) ;
}
2017-02-01 20:41:05 -03:00
}
return OK ;
}